@orbinum/sdk 1.4.0 → 2.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
@@ -109,6 +109,7 @@ __export(index_exports, {
109
109
  clearKnownEphWindow: () => clearKnownEphWindow,
110
110
  clearSession: () => clearSession,
111
111
  collectNullifiersToQuery: () => collectNullifiersToQuery,
112
+ collectOutgoingFacts: () => collectOutgoingFacts,
112
113
  collectScanEntries: () => collectScanEntries,
113
114
  commitmentHexOf: () => commitmentHexOf,
114
115
  computeNoteCommitment: () => computeNoteCommitment,
@@ -184,6 +185,7 @@ __export(index_exports, {
184
185
  getSubstrateSignerFromExtension: () => import_pjs_signer.getPolkadotSignerFromPjs,
185
186
  hasCachedSession: () => hasCachedSession,
186
187
  hasInjectedExtensions: () => hasInjectedExtensions,
188
+ hasSourcePk: () => hasSourcePk,
187
189
  hexToBigint: () => hexToBigint,
188
190
  hexToNumber: () => hexToNumber,
189
191
  implicitSubstrateToEvm: () => implicitSubstrateToEvm,
@@ -195,6 +197,7 @@ __export(index_exports, {
195
197
  isConnectionLossError: () => isConnectionLossError,
196
198
  isEvmAddress: () => isEvmAddress,
197
199
  isGhostNoteError: () => isGhostNoteError,
200
+ isHexOfLength: () => isHexOfLength,
198
201
  isImplicitEvmAccount: () => isImplicitEvmAccount,
199
202
  isNativeAsset: () => isNativeAsset,
200
203
  isNoteSelfConsistent: () => isNoteSelfConsistent,
@@ -207,6 +210,7 @@ __export(index_exports, {
207
210
  mapExtrinsicArgs: () => mapExtrinsicArgs,
208
211
  mapZkEventData: () => mapZkEventData,
209
212
  markInputsSpent: () => markInputsSpent,
213
+ mergeProvenance: () => mergeProvenance,
210
214
  normalizeChainFingerprint: () => normalizeChainFingerprint,
211
215
  normalizeEvmAddress: () => normalizeEvmAddress,
212
216
  normalizeNote: () => normalizeNote,
@@ -221,6 +225,7 @@ __export(index_exports, {
221
225
  noteTxKind: () => noteTxKind,
222
226
  openOutgoingBlob: () => openOutgoingBlob,
223
227
  openPaymentSlip: () => openPaymentSlip,
228
+ outranks: () => outranks,
224
229
  pairwiseEphWindow: () => pairwiseEphWindow,
225
230
  palletErrorKind: () => palletErrorKind,
226
231
  parseAmount: () => parseAmount,
@@ -235,6 +240,7 @@ __export(index_exports, {
235
240
  recoverOwnerPkPoint: () => recoverOwnerPkPoint,
236
241
  recoverSelfStealthNote: () => recoverSelfStealthNote,
237
242
  refuseIfAlreadySpent: () => refuseIfAlreadySpent,
243
+ regeneratePaymentSlip: () => regeneratePaymentSlip,
238
244
  removeByCommitment: () => removeByCommitment,
239
245
  requireSessionKeys: () => requireSessionKeys,
240
246
  reservePairwiseIndex: () => reservePairwiseIndex,
@@ -248,6 +254,8 @@ __export(index_exports, {
248
254
  scanAbortError: () => scanAbortError,
249
255
  sealOutgoingBlob: () => sealOutgoingBlob,
250
256
  sealPaymentSlip: () => sealPaymentSlip,
257
+ selectDescribingNote: () => selectDescribingNote,
258
+ selectDescribingNoteByCommitment: () => selectDescribingNoteByCommitment,
251
259
  selectGhosts: () => selectGhosts,
252
260
  selectNotes: () => selectNotes,
253
261
  selfEphWindow: () => selfEphWindow,
@@ -285,6 +293,9 @@ __export(index_exports, {
285
293
  module.exports = __toCommonJS(index_exports);
286
294
 
287
295
  // src/foundation/encoding/hex.ts
296
+ function isHexOfLength(value, byteLen) {
297
+ return typeof value === "string" && new RegExp(`^0x[0-9a-fA-F]{${byteLen * 2}}$`).test(value);
298
+ }
288
299
  function toHex(bytes) {
289
300
  return "0x" + Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
290
301
  }
@@ -293,11 +304,12 @@ function fromHex(hex) {
293
304
  if (clean.length % 2 !== 0) {
294
305
  throw new Error(`Invalid hex string \u2014 odd length: "${hex}"`);
295
306
  }
307
+ if (!/^[0-9a-fA-F]*$/.test(clean)) {
308
+ throw new Error("Invalid hex string \u2014 non-hex characters");
309
+ }
296
310
  const bytes = new Uint8Array(clean.length / 2);
297
311
  for (let i = 0; i < bytes.length; i++) {
298
- const byte = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
299
- if (isNaN(byte)) throw new Error(`Invalid hex character at position ${i * 2}`);
300
- bytes[i] = byte;
312
+ bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
301
313
  }
302
314
  return bytes;
303
315
  }
@@ -315,7 +327,13 @@ function scalarToHex(value) {
315
327
  }
316
328
 
317
329
  // src/foundation/encoding/bytes.ts
330
+ function assert32ByteRange(n, fn) {
331
+ if (n < 0n || n >= 1n << 256n) {
332
+ throw new Error(`${fn}: value does not fit in 32 bytes: ${n}`);
333
+ }
334
+ }
318
335
  function bigintTo32Le(n) {
336
+ assert32ByteRange(n, "bigintTo32Le");
319
337
  const buf = new Uint8Array(32);
320
338
  let v = n;
321
339
  for (let i = 0; i < 32; i++) {
@@ -332,6 +350,7 @@ function bytesToBigintLE(bytes) {
332
350
  return result;
333
351
  }
334
352
  function bigintTo32Be(n) {
353
+ assert32ByteRange(n, "bigintTo32Be");
335
354
  const buf = new Uint8Array(32);
336
355
  let v = n;
337
356
  for (let i = 31; i >= 0 && v > 0n; i--) {
@@ -341,6 +360,7 @@ function bigintTo32Be(n) {
341
360
  return buf;
342
361
  }
343
362
  function bigintTo32LeArr(n) {
363
+ assert32ByteRange(n, "bigintTo32LeArr");
344
364
  const out = new Array(32).fill(0);
345
365
  let v = n;
346
366
  for (let i = 0; i < 32; i++) {
@@ -827,7 +847,7 @@ var import_sha22 = require("@noble/hashes/sha2.js");
827
847
  var KEY_DOMAIN = new TextEncoder().encode("orbinum-note-encryption-v1");
828
848
  var VIEW_TAG_DOMAIN = new TextEncoder().encode("orbinum-view-tag-v1");
829
849
  var MEMO_PLAINTEXT_SIZE = 120;
830
- function serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk, circuitVersion) {
850
+ function serializeMemo(value, ownerPk, blinding, assetId, sourcePk, circuitVersion) {
831
851
  const buf = new Uint8Array(MEMO_PLAINTEXT_SIZE);
832
852
  const view = new DataView(buf.buffer);
833
853
  view.setBigUint64(0, value & 0xffffffffffffffffn, true);
@@ -835,7 +855,7 @@ function serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk, circui
835
855
  buf.set(ownerPk.slice(0, 32), 16);
836
856
  buf.set(blinding.slice(0, 32), 48);
837
857
  view.setUint32(80, assetId >>> 0, true);
838
- buf.set(counterpartyPk.slice(0, 32), 84);
858
+ buf.set(sourcePk.slice(0, 32), 84);
839
859
  view.setUint32(116, circuitVersion >>> 0, true);
840
860
  return buf;
841
861
  }
@@ -873,9 +893,9 @@ function parsePlaintext(nonce, ciphertextWithMac, encKey) {
873
893
  const ownerPk = bytesToBigintLE(plaintext.slice(16, 48));
874
894
  const blinding = bytesToBigintLE(plaintext.slice(48, 80));
875
895
  const assetId = BigInt(view.getUint32(80, true));
876
- const counterpartyPk = bytesToBigintLE(plaintext.slice(84, 116));
896
+ const sourcePk = bytesToBigintLE(plaintext.slice(84, 116));
877
897
  const circuitVersion = view.getUint32(116, true);
878
- return { value, ownerPk, blinding, assetId, counterpartyPk, circuitVersion };
898
+ return { value, ownerPk, blinding, assetId, sourcePk, circuitVersion };
879
899
  } catch {
880
900
  return null;
881
901
  }
@@ -893,19 +913,19 @@ var EncryptedMemo = {
893
913
  * (from PrivacyKeyManager.getViewingPublicKeyPacked() or
894
914
  * decoded from a privacy address).
895
915
  * Pass `new Uint8Array(32)` (all zeros) for a publicly-readable memo.
896
- * @param counterpartyPk 32-byte counterparty BJJ Ax. Default: all zeros.
916
+ * @param sourcePk 32-byte counterparty BJJ Ax. Default: all zeros.
897
917
  * @param circuitVersion ZK circuit version the note is spent under. Default: 0.
898
918
  * @param ephSkOverride 32-byte ephemeral secret key (stealth coordination). Optional.
899
919
  * @returns 180-byte encrypted memo: nonce(12) || ciphertext+MAC(136) || ephPk(32).
900
920
  */
901
- encrypt(value, ownerPk, blinding, assetId, commitment, recipientIvkPacked, counterpartyPk = new Uint8Array(32), circuitVersion = 0, ephSkOverride) {
921
+ encrypt(value, ownerPk, blinding, assetId, commitment, recipientIvkPacked, sourcePk = new Uint8Array(32), circuitVersion = 0, ephSkOverride) {
902
922
  const nonce = (0, import_utils.randomBytes)(NONCE_SIZE);
903
923
  const plaintext = serializeMemo(
904
924
  value,
905
925
  ownerPk,
906
926
  blinding,
907
927
  assetId,
908
- counterpartyPk,
928
+ sourcePk,
909
929
  circuitVersion
910
930
  );
911
931
  const isZeroKey = recipientIvkPacked.every((b) => b === 0);
@@ -1117,10 +1137,72 @@ var import_utils3 = require("@noble/ciphers/utils.js");
1117
1137
  var import_hkdf3 = require("@noble/hashes/hkdf.js");
1118
1138
  var import_sha24 = require("@noble/hashes/sha2.js");
1119
1139
  var import_baby_jubjub4 = require("@zk-kit/baby-jubjub");
1140
+
1141
+ // src/protocol/spend/coinSelection.ts
1142
+ var TRANSFER_TREE_DEPTH = 20;
1143
+ var LEAVES_PER_TREE = 1 << TRANSFER_TREE_DEPTH;
1144
+ function isValidLeafIndex(leafIndex) {
1145
+ return leafIndex !== null && leafIndex !== void 0 && Number.isSafeInteger(leafIndex) && leafIndex >= 0 && leafIndex < 2 ** 32;
1146
+ }
1147
+ function treeIdOf(note) {
1148
+ const idx = note.leafIndex;
1149
+ return isValidLeafIndex(idx) ? Math.floor(idx / LEAVES_PER_TREE) : 0;
1150
+ }
1151
+ function isSpendable(note) {
1152
+ return !note.spent && note.value > 0n;
1153
+ }
1154
+ function canPairWith(a, b) {
1155
+ return a.circuitVersion === b.circuitVersion && treeIdOf(a) === treeIdOf(b);
1156
+ }
1157
+ function selectNotes(notes, needed) {
1158
+ const unspent = notes.filter(isSpendable);
1159
+ const sorted = [...unspent].sort((a, b) => a.value < b.value ? -1 : 1);
1160
+ const single = sorted.find((n) => n.value >= needed);
1161
+ if (single) return [single, null];
1162
+ for (let i = 0; i < sorted.length; i++) {
1163
+ for (let j = i + 1; j < sorted.length; j++) {
1164
+ const a = sorted[i];
1165
+ const b = sorted[j];
1166
+ if (a !== void 0 && b !== void 0 && canPairWith(a, b) && a.value + b.value >= needed) {
1167
+ return [a, b];
1168
+ }
1169
+ }
1170
+ }
1171
+ for (let i = 0; i < sorted.length; i++) {
1172
+ for (let j = i + 1; j < sorted.length; j++) {
1173
+ const a = sorted[i];
1174
+ const b = sorted[j];
1175
+ if (a !== void 0 && b !== void 0 && a.circuitVersion === b.circuitVersion && a.value + b.value >= needed) {
1176
+ return { needsConsolidation: true };
1177
+ }
1178
+ }
1179
+ }
1180
+ return null;
1181
+ }
1182
+ function buildDummyTransferInput(assetId) {
1183
+ const zeroSibling = "0x" + "00".repeat(32);
1184
+ return {
1185
+ nullifier: 0n,
1186
+ // Constraint 9: nullifier * is_dummy.out === 0 → must be 0
1187
+ value: 0n,
1188
+ // triggers is_dummy[i].out = 1 in the circuit
1189
+ assetId,
1190
+ // must match real note (Constraint 7)
1191
+ ownerPk: 0n,
1192
+ blinding: 0n,
1193
+ spendingKey: 1n,
1194
+ // arbitrary; EdDSA is disabled (enabled = 0) for dummy inputs
1195
+ pathSiblings: Array(TRANSFER_TREE_DEPTH).fill(zeroSibling),
1196
+ leafIndex: 0
1197
+ };
1198
+ }
1199
+
1200
+ // src/protocol/memo/PaymentSlip.ts
1120
1201
  var SLIP_DOMAIN = new TextEncoder().encode("orbinum-payment-slip-v1");
1121
1202
  var NONCE_PREFIX2 = new TextEncoder().encode("SLP1");
1122
1203
  var EPH_PK_SIZE2 = 32;
1123
1204
  var NONCE_SUFFIX_SIZE2 = 8;
1205
+ var MAX_SLIP_ENVELOPE_SIZE = 4096;
1124
1206
  function deriveSlipKey(sharedSecret) {
1125
1207
  return (0, import_hkdf3.hkdf)(import_sha24.sha256, sharedSecret, void 0, SLIP_DOMAIN, 32);
1126
1208
  }
@@ -1155,6 +1237,7 @@ function sealPaymentSlip(recipientIvkPacked, fields) {
1155
1237
  }
1156
1238
  function openPaymentSlip(recipientIvsk, envelope) {
1157
1239
  if (envelope.length < EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE2 + 16) return null;
1240
+ if (envelope.length > MAX_SLIP_ENVELOPE_SIZE) return null;
1158
1241
  try {
1159
1242
  const ephPkPacked = envelope.subarray(0, EPH_PK_SIZE2);
1160
1243
  const ephPkPoint = (0, import_baby_jubjub4.unpackPoint)(bytesToBigintLE(ephPkPacked));
@@ -1166,11 +1249,19 @@ function openPaymentSlip(recipientIvsk, envelope) {
1166
1249
  const sealed = envelope.subarray(EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE2);
1167
1250
  const cipher = (0, import_chacha3.chacha20poly1305)(slipKey, buildNonce2(suffix));
1168
1251
  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;
1252
+ const parsed = JSON.parse(new TextDecoder().decode(plaintext));
1253
+ if (!isHexOfLength(parsed["commitmentHex"], 32)) return null;
1254
+ if (!isHexOfLength(parsed["encryptedMemo"], ENCRYPTED_MEMO_SIZE)) return null;
1255
+ const leafIndex = parsed["leafIndex"];
1256
+ if (leafIndex !== void 0 && !isValidLeafIndex(leafIndex)) return null;
1257
+ const rawTxHash = parsed["txHash"];
1258
+ const txHash = isHexOfLength(rawTxHash, 32) ? rawTxHash : void 0;
1259
+ return {
1260
+ commitmentHex: parsed["commitmentHex"],
1261
+ encryptedMemo: parsed["encryptedMemo"],
1262
+ ...leafIndex !== void 0 ? { leafIndex } : {},
1263
+ ...txHash !== void 0 ? { txHash } : {}
1264
+ };
1174
1265
  } catch {
1175
1266
  return null;
1176
1267
  }
@@ -1299,14 +1390,14 @@ var NoteBuilder = class {
1299
1390
  const ownerPk = input.ownerPk ?? 0n;
1300
1391
  const blinding = input.blinding ?? BigInt(Date.now());
1301
1392
  const spendingKey = input.spendingKey ?? 0n;
1302
- const counterpartyPk = input.counterpartyPk ?? 0n;
1393
+ const sourcePk = input.sourcePk ?? 0n;
1303
1394
  const circuitVersion = input.circuitVersion ?? CURRENT_CIRCUIT_VERSION;
1304
1395
  const useStealth = input.viewingPublicKey !== void 0 && input.recipientOwnerPk !== void 0;
1305
1396
  let memo;
1306
1397
  if (useStealth) {
1307
1398
  const recipientOwnerPk = input.recipientOwnerPk;
1308
1399
  const recipientIvkPacked = input.viewingPublicKey;
1309
- const ephSk = (0, import_utils4.randomBytes)(32);
1400
+ const ephSk = input.ephSkOverride ?? (0, import_utils4.randomBytes)(32);
1310
1401
  const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
1311
1402
  const ivkPoint = (0, import_baby_jubjub7.unpackPoint)(ivkPackedBigint);
1312
1403
  if (!ivkPoint)
@@ -1334,7 +1425,7 @@ var NoteBuilder = class {
1334
1425
  Number(assetId),
1335
1426
  stealthCommitmentBytes,
1336
1427
  recipientIvkPacked,
1337
- bigintTo32Le(counterpartyPk),
1428
+ bigintTo32Le(sourcePk),
1338
1429
  circuitVersion,
1339
1430
  ephSk
1340
1431
  )
@@ -1376,7 +1467,7 @@ var NoteBuilder = class {
1376
1467
  commitmentHex: toHex(commitmentBytes2),
1377
1468
  nullifierHex: toHex(nullifierBytes2),
1378
1469
  memo,
1379
- counterpartyPk,
1470
+ sourcePk,
1380
1471
  ...ovkBlob ? { ovkBlob } : {}
1381
1472
  };
1382
1473
  }
@@ -1392,7 +1483,7 @@ var NoteBuilder = class {
1392
1483
  Number(assetId),
1393
1484
  commitmentBytes,
1394
1485
  input.viewingPublicKey,
1395
- bigintTo32Le(counterpartyPk),
1486
+ bigintTo32Le(sourcePk),
1396
1487
  circuitVersion,
1397
1488
  input.ephSkOverride
1398
1489
  )
@@ -1415,7 +1506,7 @@ var NoteBuilder = class {
1415
1506
  commitmentHex: toHex(commitmentBytes),
1416
1507
  nullifierHex: toHex(nullifierBytes),
1417
1508
  memo,
1418
- counterpartyPk
1509
+ sourcePk
1419
1510
  };
1420
1511
  }
1421
1512
  /**
@@ -1427,10 +1518,10 @@ var NoteBuilder = class {
1427
1518
  * @param note The ZkNote whose fields populate the plaintext.
1428
1519
  * @param recipientIvkPacked 32-byte LE packed BJJ viewing public key of the recipient.
1429
1520
  * Pass `new Uint8Array(32)` (default) for a public/dummy memo.
1430
- * @param counterpartyPk 32-byte counterparty BabyJubJub Ax.
1521
+ * @param sourcePk 32-byte counterparty BabyJubJub Ax.
1431
1522
  * Pass `new Uint8Array(32)` (default) for no counterparty.
1432
1523
  */
1433
- static buildMemo(note, recipientIvkPacked, counterpartyPk) {
1524
+ static buildMemo(note, recipientIvkPacked, sourcePk) {
1434
1525
  return EncryptedMemo.encrypt(
1435
1526
  note.value,
1436
1527
  bigintTo32Le(note.ownerPk),
@@ -1438,7 +1529,7 @@ var NoteBuilder = class {
1438
1529
  Number(note.assetId),
1439
1530
  bigintTo32Le(note.commitment),
1440
1531
  recipientIvkPacked ?? new Uint8Array(32),
1441
- counterpartyPk ?? bigintTo32Le(note.counterpartyPk ?? 0n),
1532
+ sourcePk ?? bigintTo32Le(note.sourcePk ?? 0n),
1442
1533
  note.circuitVersion
1443
1534
  );
1444
1535
  }
@@ -1446,67 +1537,6 @@ var NoteBuilder = class {
1446
1537
 
1447
1538
  // src/protocol/note/NoteDecryptor.ts
1448
1539
  var import_poseidon_lite2 = require("poseidon-lite");
1449
-
1450
- // src/protocol/spend/coinSelection.ts
1451
- var TRANSFER_TREE_DEPTH = 20;
1452
- var LEAVES_PER_TREE = 1 << TRANSFER_TREE_DEPTH;
1453
- function isValidLeafIndex(leafIndex) {
1454
- return leafIndex !== null && leafIndex !== void 0 && Number.isSafeInteger(leafIndex) && leafIndex >= 0 && leafIndex < 2 ** 32;
1455
- }
1456
- function treeIdOf(note) {
1457
- const idx = note.leafIndex;
1458
- return isValidLeafIndex(idx) ? Math.floor(idx / LEAVES_PER_TREE) : 0;
1459
- }
1460
- function isSpendable(note) {
1461
- return !note.spent && note.value > 0n;
1462
- }
1463
- function canPairWith(a, b) {
1464
- return a.circuitVersion === b.circuitVersion && treeIdOf(a) === treeIdOf(b);
1465
- }
1466
- function selectNotes(notes, needed) {
1467
- const unspent = notes.filter(isSpendable);
1468
- const sorted = [...unspent].sort((a, b) => a.value < b.value ? -1 : 1);
1469
- const single = sorted.find((n) => n.value >= needed);
1470
- if (single) return [single, null];
1471
- for (let i = 0; i < sorted.length; i++) {
1472
- for (let j = i + 1; j < sorted.length; j++) {
1473
- const a = sorted[i];
1474
- const b = sorted[j];
1475
- if (a !== void 0 && b !== void 0 && canPairWith(a, b) && a.value + b.value >= needed) {
1476
- return [a, b];
1477
- }
1478
- }
1479
- }
1480
- for (let i = 0; i < sorted.length; i++) {
1481
- for (let j = i + 1; j < sorted.length; j++) {
1482
- const a = sorted[i];
1483
- const b = sorted[j];
1484
- if (a !== void 0 && b !== void 0 && a.circuitVersion === b.circuitVersion && a.value + b.value >= needed) {
1485
- return { needsConsolidation: true };
1486
- }
1487
- }
1488
- }
1489
- return null;
1490
- }
1491
- function buildDummyTransferInput(assetId) {
1492
- const zeroSibling = "0x" + "00".repeat(32);
1493
- return {
1494
- nullifier: 0n,
1495
- // Constraint 9: nullifier * is_dummy.out === 0 → must be 0
1496
- value: 0n,
1497
- // triggers is_dummy[i].out = 1 in the circuit
1498
- assetId,
1499
- // must match real note (Constraint 7)
1500
- ownerPk: 0n,
1501
- blinding: 0n,
1502
- spendingKey: 1n,
1503
- // arbitrary; EdDSA is disabled (enabled = 0) for dummy inputs
1504
- pathSiblings: Array(TRANSFER_TREE_DEPTH).fill(zeroSibling),
1505
- leafIndex: 0
1506
- };
1507
- }
1508
-
1509
- // src/protocol/note/NoteDecryptor.ts
1510
1540
  function decryptAndVerifyPlaintext(memoBytes, commitmentBytes, sharedSecret, effectiveOwnerPk) {
1511
1541
  const plaintext = EncryptedMemo.decryptWithSharedSecret(
1512
1542
  memoBytes,
@@ -1597,7 +1627,7 @@ function tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwn
1597
1627
  commitmentHex: toHex(bigintTo32Le(recomputed)),
1598
1628
  nullifierHex: toHex(bigintTo32Le(nullifier)),
1599
1629
  memo: Array.from(memoBytes),
1600
- counterpartyPk: plaintext.counterpartyPk
1630
+ sourcePk: plaintext.sourcePk
1601
1631
  }
1602
1632
  };
1603
1633
  }
@@ -1631,10 +1661,19 @@ function tryRecoverOutgoing(hint, ovk, opts) {
1631
1661
  assetId: plaintext.assetId,
1632
1662
  recipientStealthPk: plaintext.ownerPk,
1633
1663
  blinding: plaintext.blinding,
1634
- counterpartyPk: plaintext.counterpartyPk,
1664
+ sourcePk: plaintext.sourcePk,
1635
1665
  circuitVersion: plaintext.circuitVersion
1636
1666
  };
1637
1667
  }
1668
+ function collectOutgoingFacts(hint) {
1669
+ if (!isHexOfLength(hint.commitmentHex, 32)) return null;
1670
+ if (!isHexOfLength(hint.encryptedMemo, ENCRYPTED_MEMO_SIZE)) return null;
1671
+ return {
1672
+ commitmentHex: hint.commitmentHex,
1673
+ ...isValidLeafIndex(hint.leafIndex) ? { leafIndex: hint.leafIndex } : {},
1674
+ encryptedMemo: hint.encryptedMemo
1675
+ };
1676
+ }
1638
1677
 
1639
1678
  // src/protocol/note/NoteDisclosure.ts
1640
1679
  var import_poseidon_lite3 = require("poseidon-lite");
@@ -2769,11 +2808,17 @@ var SubstrateClient = class _SubstrateClient {
2769
2808
 
2770
2809
  // src/chain/evm/EvmClient.ts
2771
2810
  var EvmClient = class {
2772
- /** @param rpcUrl - HTTP URL of the EVM JSON-RPC endpoint (e.g. `"http://localhost:9933"`). */
2773
- constructor(rpcUrl) {
2811
+ /**
2812
+ * @param rpcUrl - HTTP URL of the EVM JSON-RPC endpoint (e.g. `"http://localhost:9933"`).
2813
+ * @param peerRpcUrl - Optional second endpoint, used only to tell a genuinely
2814
+ * pending transaction from one stranded on `rpcUrl` alone. See `waitForReceipt`.
2815
+ */
2816
+ constructor(rpcUrl, peerRpcUrl) {
2774
2817
  this.rpcUrl = rpcUrl;
2818
+ this.peerRpcUrl = peerRpcUrl;
2775
2819
  }
2776
2820
  rpcUrl;
2821
+ peerRpcUrl;
2777
2822
  /**
2778
2823
  * Performs a single JSON-RPC call and returns the typed result.
2779
2824
  * Throws on HTTP errors, RPC-level errors, or a `null` result.
@@ -2831,10 +2876,18 @@ var EvmClient = class {
2831
2876
  const hex = await this.request("eth_getTransactionCount", [address, "latest"]);
2832
2877
  return hexToNumber(hex);
2833
2878
  }
2834
- /** Returns the current gas price in wei. */
2835
- async getGasPrice() {
2879
+ /**
2880
+ * Returns the current gas price in wei, padded by `bumpPercent`.
2881
+ *
2882
+ * `eth_gasPrice` reports the base fee exactly, and the base fee moves
2883
+ * between signing and the pool's next revalidation. A transaction priced at
2884
+ * the bare minimum is evicted as `GasPriceTooLow` the moment it rises, which
2885
+ * leaves every later nonce from that account stranded in the future queue.
2886
+ * The default 25% pad absorbs the usual movement.
2887
+ */
2888
+ async getGasPrice(bumpPercent = 25) {
2836
2889
  const hex = await this.request("eth_gasPrice", []);
2837
- return hexToBigint(hex);
2890
+ return hexToBigint(hex) * BigInt(100 + bumpPercent) / 100n;
2838
2891
  }
2839
2892
  /** Submits a signed raw transaction. Returns the transaction hash. */
2840
2893
  async sendRawTransaction(signedHex) {
@@ -2952,10 +3005,41 @@ var EvmClient = class {
2952
3005
  deadline = Math.min(deadline + timeoutMs, hardDeadline);
2953
3006
  }
2954
3007
  }
3008
+ if (await this.isStrandedOnThisNode(txHash)) {
3009
+ throw new Error(
3010
+ `Transaction dropped from the tx pool (never propagated beyond the submitting node, ${Date.now() - start}ms): ${txHash}`
3011
+ );
3012
+ }
2955
3013
  throw new Error(
2956
3014
  `Transaction still pending after ${Date.now() - start}ms: ${txHash} \u2014 it may still confirm; check the hash on the explorer before retrying`
2957
3015
  );
2958
3016
  }
3017
+ /**
3018
+ * True when `rpcUrl` knows the transaction but the configured peer does not.
3019
+ *
3020
+ * Returns false without a peer configured, and on any peer error — an
3021
+ * unreachable peer is not evidence that a live transaction is stranded.
3022
+ */
3023
+ async isStrandedOnThisNode(txHash) {
3024
+ if (!this.peerRpcUrl) return false;
3025
+ try {
3026
+ const res = await postJsonWithRetry(
3027
+ this.peerRpcUrl,
3028
+ JSON.stringify({
3029
+ id: 1,
3030
+ jsonrpc: "2.0",
3031
+ method: "eth_getTransactionByHash",
3032
+ params: [txHash]
3033
+ })
3034
+ );
3035
+ if (!res.ok) return false;
3036
+ const json = await res.json();
3037
+ if (json.error) return false;
3038
+ return json.result === null;
3039
+ } catch {
3040
+ return false;
3041
+ }
3042
+ }
2959
3043
  };
2960
3044
 
2961
3045
  // src/chain/evm/explorer/EvmExplorer.ts
@@ -3674,6 +3758,23 @@ function padTo32Multiple(data) {
3674
3758
  padded.set(data);
3675
3759
  return padded;
3676
3760
  }
3761
+ function bytes32Slot(value) {
3762
+ if (value.length !== 32) {
3763
+ throw new Error(`encodeAbi: bytes32 needs 32 bytes, got ${value.length}`);
3764
+ }
3765
+ const slot = new Uint8Array(32);
3766
+ slot.set(value);
3767
+ return slot;
3768
+ }
3769
+ function addressSlot(address) {
3770
+ const clean = address.startsWith("0x") ? address.slice(2) : address;
3771
+ if (clean.length > 40) {
3772
+ throw new Error(`encodeAbi: address needs at most 20 bytes, got ${clean.length / 2}`);
3773
+ }
3774
+ const slot = new Uint8Array(32);
3775
+ slot.set(fromHex("0x" + clean.padStart(40, "0")), 12);
3776
+ return slot;
3777
+ }
3677
3778
  function encodeStaticParam(param) {
3678
3779
  const buf = new Uint8Array(32);
3679
3780
  switch (param.type) {
@@ -3681,14 +3782,10 @@ function encodeStaticParam(param) {
3681
3782
  return bigintTo32Be(param.value);
3682
3783
  }
3683
3784
  case "bytes32": {
3684
- buf.set(param.value.slice(0, 32));
3685
- return buf;
3785
+ return bytes32Slot(param.value);
3686
3786
  }
3687
3787
  case "address": {
3688
- const clean = param.value.startsWith("0x") ? param.value.slice(2) : param.value;
3689
- const bytes = fromHex("0x" + clean.padStart(40, "0"));
3690
- buf.set(bytes, 12);
3691
- return buf;
3788
+ return addressSlot(param.value);
3692
3789
  }
3693
3790
  case "bool": {
3694
3791
  buf[31] = param.value ? 1 : 0;
@@ -3711,25 +3808,13 @@ function encodeDynamicParam(param) {
3711
3808
  return concat([bigintTo32Be(BigInt(data.length)), padTo32Multiple(data)]);
3712
3809
  }
3713
3810
  case "bytes32[]": {
3714
- const n = param.value.length;
3715
- const parts = [bigintTo32Be(BigInt(n))];
3716
- for (const b32 of param.value) {
3717
- const slot = new Uint8Array(32);
3718
- slot.set(b32.slice(0, 32));
3719
- parts.push(slot);
3720
- }
3811
+ const parts = [bigintTo32Be(BigInt(param.value.length))];
3812
+ for (const b32 of param.value) parts.push(bytes32Slot(b32));
3721
3813
  return concat(parts);
3722
3814
  }
3723
3815
  case "address[]": {
3724
- const n = param.value.length;
3725
- const parts = [bigintTo32Be(BigInt(n))];
3726
- for (const addr of param.value) {
3727
- const slot = new Uint8Array(32);
3728
- const clean = addr.startsWith("0x") ? addr.slice(2) : addr;
3729
- const bytes = fromHex("0x" + clean.padStart(40, "0"));
3730
- slot.set(bytes, 12);
3731
- parts.push(slot);
3732
- }
3816
+ const parts = [bigintTo32Be(BigInt(param.value.length))];
3817
+ for (const addr of param.value) parts.push(addressSlot(addr));
3733
3818
  return concat(parts);
3734
3819
  }
3735
3820
  case "bytes[]": {
@@ -3785,6 +3870,122 @@ function decodeUint(data, offset = 0) {
3785
3870
  return result;
3786
3871
  }
3787
3872
 
3873
+ // src/chain/evm/precompiles/shieldedPoolCalldata.ts
3874
+ var CLAIM_PUBLIC_SIGNALS_SIZE = 76;
3875
+ function bytes32(hex, field) {
3876
+ if (!isHexOfLength(hex, 32)) {
3877
+ throw new Error(`${field}: expected a 0x-prefixed 32-byte hex string, got ${hex}`);
3878
+ }
3879
+ return fromHex(hex);
3880
+ }
3881
+ function uint32(value, field) {
3882
+ if (!Number.isInteger(value) || value < 0 || value > 4294967295) {
3883
+ throw new Error(`${field}: expected a uint32 (0..4294967295), got ${value}`);
3884
+ }
3885
+ return BigInt(value);
3886
+ }
3887
+ function accountId32(address, field) {
3888
+ const raw = address.startsWith("0x") ? address.slice(2) : address;
3889
+ if (raw.length > 64) {
3890
+ throw new Error(`${field}: expected at most 32 bytes, got ${raw.length / 2}`);
3891
+ }
3892
+ return bytes32("0x" + raw.padEnd(64, "0"), field);
3893
+ }
3894
+ function buildShieldCalldata(params) {
3895
+ EncryptedMemo.validate(params.encryptedMemo, "buildShieldCalldata.encryptedMemo");
3896
+ const commitment = bytes32(params.commitment, "buildShieldCalldata.commitment");
3897
+ return encodeHex(
3898
+ SP_SEL.SHIELD,
3899
+ { type: "uint", value: uint32(params.assetId, "buildShieldCalldata.assetId") },
3900
+ { type: "bytes32", value: commitment },
3901
+ { type: "bytes", value: params.encryptedMemo }
3902
+ );
3903
+ }
3904
+ function buildPrivateTransferCalldata(params) {
3905
+ const nullifiers = params.inputs.map(
3906
+ (input, i) => bytes32(input.nullifier, `buildPrivateTransferCalldata.inputs[${i}].nullifier`)
3907
+ );
3908
+ const commitments = params.outputs.map(
3909
+ (output, i) => bytes32(output.commitment, `buildPrivateTransferCalldata.outputs[${i}].commitment`)
3910
+ );
3911
+ const memos = params.outputs.map((output, i) => {
3912
+ EncryptedMemo.validate(
3913
+ output.encryptedMemo,
3914
+ `buildPrivateTransferCalldata.outputs[${i}].encryptedMemo`
3915
+ );
3916
+ return output.encryptedMemo;
3917
+ });
3918
+ const root = bytes32(params.merkleRoot, "buildPrivateTransferCalldata.merkleRoot");
3919
+ return encodeHex(
3920
+ SP_SEL.PRIVATE_TRANSFER,
3921
+ { type: "bytes", value: params.proof },
3922
+ { type: "bytes32", value: root },
3923
+ { type: "bytes32[]", value: nullifiers },
3924
+ { type: "bytes32[]", value: commitments },
3925
+ { type: "bytes[]", value: memos },
3926
+ { type: "uint", value: uint32(params.assetId, "buildPrivateTransferCalldata.assetId") },
3927
+ { type: "uint", value: params.fee ?? 0n },
3928
+ {
3929
+ type: "uint",
3930
+ value: uint32(params.circuitVersion, "buildPrivateTransferCalldata.circuitVersion")
3931
+ }
3932
+ );
3933
+ }
3934
+ function buildUnshieldCalldata(params) {
3935
+ const root = bytes32(params.merkleRoot, "buildUnshieldCalldata.merkleRoot");
3936
+ const nullifier = bytes32(params.nullifier, "buildUnshieldCalldata.nullifier");
3937
+ const recipient = accountId32(
3938
+ params.recipientAddress,
3939
+ "buildUnshieldCalldata.recipientAddress"
3940
+ );
3941
+ const changeCommitment = bytes32(
3942
+ params.changeCommitment ?? "0x" + "00".repeat(32),
3943
+ "buildUnshieldCalldata.changeCommitment"
3944
+ );
3945
+ const changeEncryptedMemo = params.changeEncryptedMemo ?? new Uint8Array();
3946
+ return encodeHex(
3947
+ SP_SEL.UNSHIELD,
3948
+ { type: "bytes", value: params.proof },
3949
+ { type: "bytes32", value: root },
3950
+ { type: "bytes32", value: nullifier },
3951
+ { type: "uint", value: uint32(params.assetId, "buildUnshieldCalldata.assetId") },
3952
+ { type: "uint", value: params.amount },
3953
+ { type: "bytes32", value: recipient },
3954
+ { type: "uint", value: params.fee ?? 0n },
3955
+ { type: "bytes32", value: changeCommitment },
3956
+ { type: "bytes", value: changeEncryptedMemo },
3957
+ {
3958
+ type: "uint",
3959
+ value: uint32(params.circuitVersion, "buildUnshieldCalldata.circuitVersion")
3960
+ }
3961
+ );
3962
+ }
3963
+ function buildClaimShieldedFeesCalldata(params) {
3964
+ EncryptedMemo.validate(params.encryptedMemo, "buildClaimShieldedFeesCalldata.encryptedMemo");
3965
+ if (params.proof.length === 0) {
3966
+ throw new Error("claimShieldedFees: proof must not be empty");
3967
+ }
3968
+ if (params.publicSignals.length !== CLAIM_PUBLIC_SIGNALS_SIZE) {
3969
+ throw new Error(
3970
+ `claimShieldedFees: publicSignals must be ${CLAIM_PUBLIC_SIGNALS_SIZE} bytes, got ${params.publicSignals.length}`
3971
+ );
3972
+ }
3973
+ const commitment = bytes32(params.commitment, "buildClaimShieldedFeesCalldata.commitment");
3974
+ return encodeHex(
3975
+ SP_SEL.CLAIM_SHIELDED_FEES,
3976
+ { type: "bytes32", value: commitment },
3977
+ { type: "uint", value: params.amount },
3978
+ { type: "uint", value: uint32(params.assetId, "buildClaimShieldedFeesCalldata.assetId") },
3979
+ { type: "bytes", value: params.encryptedMemo },
3980
+ { type: "bytes", value: params.proof },
3981
+ { type: "bytes", value: params.publicSignals },
3982
+ {
3983
+ type: "uint",
3984
+ value: uint32(params.circuitVersion, "buildClaimShieldedFeesCalldata.circuitVersion")
3985
+ }
3986
+ );
3987
+ }
3988
+
3788
3989
  // src/chain/evm/precompiles/ShieldedPoolPrecompile.ts
3789
3990
  var ShieldedPoolPrecompile = class {
3790
3991
  constructor(evm) {
@@ -3792,228 +3993,111 @@ var ShieldedPoolPrecompile = class {
3792
3993
  }
3793
3994
  evm;
3794
3995
  addr = PRECOMPILE_ADDR.SHIELDED_POOL;
3795
- // ─── shield ────────────────────────────────────────────────────────────────
3796
- /**
3797
- * Returns the ABI-encoded calldata for `shield(uint32, bytes32, bytes)`.
3798
- * The token amount must be sent as `msg.value` (the `value` field of the EVM
3799
- * transaction) this is what MetaMask and other wallets display to the user.
3800
- */
3996
+ // ─── Calldata ────────────────────────────────────────────────────────────
3997
+ //
3998
+ // Thin delegates to `shieldedPoolCalldata`, kept because they are public
3999
+ // API. New code should import those functions directly: they are pure, so
4000
+ // using them needs no `EvmClient` to construct.
3801
4001
  buildShieldCalldata(params) {
3802
- EncryptedMemo.validate(params.encryptedMemo, "buildShieldCalldata.encryptedMemo");
3803
- const commitment = fromHex(params.commitment);
3804
- return encodeHex(
3805
- SP_SEL.SHIELD,
3806
- { type: "uint", value: BigInt(params.assetId) },
3807
- { type: "bytes32", value: commitment },
3808
- { type: "bytes", value: params.encryptedMemo }
3809
- );
4002
+ return buildShieldCalldata(params);
4003
+ }
4004
+ buildPrivateTransferCalldata(params) {
4005
+ return buildPrivateTransferCalldata(params);
4006
+ }
4007
+ buildUnshieldCalldata(params) {
4008
+ return buildUnshieldCalldata(params);
3810
4009
  }
4010
+ buildClaimShieldedFeesCalldata(params) {
4011
+ return buildClaimShieldedFeesCalldata(params);
4012
+ }
4013
+ // ─── shield ──────────────────────────────────────────────────────────────
3811
4014
  /**
3812
4015
  * Deposits tokens into the shielded pool from a payable EVM transaction.
3813
4016
  *
3814
- * The token amount is sent as `msg.value` so EVM wallets (MetaMask, etc.) display
3815
- * the correct amount on the confirmation screen. The precompile dispatches
3816
- * `shieldedPool.shield` with its own address as origin, so the funds flow:
3817
- * caller precompile (via msg.value, handled by EVM)
3818
- * precompile → pool (via pallet transfer)
3819
- * This avoids double-deduction while keeping the displayed amount accurate.
4017
+ * The amount rides as `msg.value` so EVM wallets show the correct figure on
4018
+ * the confirmation screen. The precompile then dispatches with its OWN
4019
+ * address as origin, so funds flow caller → precompile → pool. That avoids
4020
+ * a double deduction while keeping the displayed amount accurate.
3820
4021
  *
3821
4022
  * Extrinsic: `shieldedPool.shield(assetId, amount, commitment, encryptedMemo)`
3822
4023
  */
3823
4024
  async shield(params, signer) {
3824
4025
  return signer({
3825
4026
  to: this.addr,
3826
- data: this.buildShieldCalldata(params),
4027
+ data: buildShieldCalldata(params),
3827
4028
  value: params.amount
3828
4029
  });
3829
4030
  }
3830
- // ─── privateTransfer ───────────────────────────────────────────────────────
3831
- /**
3832
- * Returns the ABI-encoded calldata for
3833
- * `privateTransfer(bytes, bytes32, bytes32[], bytes32[], bytes[], uint32, uint256, uint32)`.
3834
- * The trailing `uint32` is the circuit version the input notes were created under.
3835
- */
3836
- buildPrivateTransferCalldata(params) {
3837
- const nullifiers = params.inputs.map((i) => fromHex(i.nullifier));
3838
- const commitments = params.outputs.map((o) => fromHex(o.commitment));
3839
- const memos = params.outputs.map((o, i) => {
3840
- EncryptedMemo.validate(
3841
- o.encryptedMemo,
3842
- `buildPrivateTransferCalldata.outputs[${i}].encryptedMemo`
3843
- );
3844
- return o.encryptedMemo;
3845
- });
3846
- const root = fromHex(params.merkleRoot);
3847
- return encodeHex(
3848
- SP_SEL.PRIVATE_TRANSFER,
3849
- { type: "bytes", value: params.proof },
3850
- { type: "bytes32", value: root },
3851
- { type: "bytes32[]", value: nullifiers },
3852
- { type: "bytes32[]", value: commitments },
3853
- { type: "bytes[]", value: memos },
3854
- { type: "uint", value: BigInt(params.assetId) },
3855
- { type: "uint", value: params.fee ?? 0n },
3856
- { type: "uint", value: BigInt(params.circuitVersion) }
3857
- );
3858
- }
4031
+ // ─── privateTransfer ─────────────────────────────────────────────────────
3859
4032
  /**
3860
- * Submits a private transfer within the shielded pool from an EVM transaction.
4033
+ * Submits a private transfer within the shielded pool.
3861
4034
  *
3862
- * The EVM caller identity is **irrelevant to the ZK proof** — the sender is
3863
- * hidden by design. Any EVM address (including a relayer) can submit a valid proof.
4035
+ * The EVM caller identity is IRRELEVANT to the ZK proof — the sender is
4036
+ * hidden by design, so any address (a relayer included) can submit a valid
4037
+ * proof.
3864
4038
  *
3865
4039
  * Extrinsic: `shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos)`
3866
4040
  */
3867
4041
  async privateTransfer(params, signer) {
3868
- return signer({ to: this.addr, data: this.buildPrivateTransferCalldata(params) });
3869
- }
3870
- // ─── unshield ──────────────────────────────────────────────────────────────
3871
- /**
3872
- * Params for an `unshield` call via the EVM precompile.
3873
- * The `recipient` is a full 32-byte AccountId32 (Substrate account or
3874
- * EeSuffix-derived: `H160 ++ [0x00; 12]`).
3875
- */
3876
- buildUnshieldCalldata(params) {
3877
- const proof = params.proof;
3878
- const root = fromHex(params.merkleRoot);
3879
- const nullifier = fromHex(params.nullifier);
3880
- const recipientRaw = params.recipientAddress.startsWith("0x") ? params.recipientAddress.slice(2) : params.recipientAddress;
3881
- const recipientBytes = fromHex(
3882
- "0x" + (recipientRaw.length === 64 ? recipientRaw : recipientRaw.padEnd(64, "0"))
3883
- );
3884
- const changeCommitmentHex = params.changeCommitment ?? "0x" + "00".repeat(32);
3885
- const changeCommitment = fromHex(changeCommitmentHex);
3886
- const changeEncryptedMemo = params.changeEncryptedMemo ?? new Uint8Array();
3887
- return encodeHex(
3888
- SP_SEL.UNSHIELD,
3889
- { type: "bytes", value: proof },
3890
- { type: "bytes32", value: root },
3891
- { type: "bytes32", value: nullifier },
3892
- { type: "uint", value: BigInt(params.assetId) },
3893
- { type: "uint", value: params.amount },
3894
- { type: "bytes32", value: recipientBytes },
3895
- { type: "uint", value: params.fee ?? 0n },
3896
- { type: "bytes32", value: changeCommitment },
3897
- { type: "bytes", value: changeEncryptedMemo },
3898
- { type: "uint", value: BigInt(params.circuitVersion) }
3899
- );
4042
+ return signer({ to: this.addr, data: buildPrivateTransferCalldata(params) });
3900
4043
  }
4044
+ // ─── unshield ────────────────────────────────────────────────────────────
3901
4045
  /**
3902
4046
  * Withdraws tokens from the shielded pool to a recipient account.
3903
4047
  *
3904
- * `params.recipientAddress` must be a 0x-prefixed 64-hex-char AccountId32.
3905
- * To send to an EVM address, use `evmToImplicitSubstrate(evmAddr)` from
3906
- * `@orbinum/sdk` to derive the AccountId32 first.
4048
+ * `params.recipientAddress` must be a 0x-prefixed AccountId32. To send to an
4049
+ * EVM address, derive it first with `evmToImplicitSubstrate(evmAddr)`.
3907
4050
  *
3908
4051
  * Extrinsic: `shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient)`
3909
4052
  */
3910
4053
  async unshield(params, signer) {
3911
- return signer({ to: this.addr, data: this.buildUnshieldCalldata(params) });
4054
+ return signer({ to: this.addr, data: buildUnshieldCalldata(params) });
3912
4055
  }
3913
- // ─── Gas estimation ────────────────────────────────────────────────────────
4056
+ // ─── claimShieldedFees ───────────────────────────────────────────────────
3914
4057
  /**
3915
- * Estimates the EVM gas for a `shield` call without submitting.
3916
- * Requires `from` to be set to the actual sender address.
4058
+ * Claims accrued relay fees as a private shielded note.
4059
+ *
4060
+ * For validators/relayers holding fees in `pallet-relayer` who want them
4061
+ * paid privately into the shielded pool rather than as a public balance
4062
+ * credit. The ZK `value_proof` binds `commitment` to
4063
+ * `(amount, assetId, ownerPk, blinding)`, so the runtime can verify the note
4064
+ * encodes exactly the claimed amount and a malicious relayer cannot inflate
4065
+ * the withdrawal.
4066
+ *
4067
+ * The `msg.sender` address is the validator identity, and must match the
4068
+ * one with pending fees.
4069
+ *
4070
+ * Extrinsic: `shieldedPool.claim_shielded_fees(commitment, amount, assetId, memo, proof, publicSignals)`
3917
4071
  */
3918
- async estimateShieldGas(params, from) {
3919
- return this.evm.estimateGas({
3920
- from,
4072
+ async claimShieldedFees(params, signer) {
4073
+ return signer({
3921
4074
  to: this.addr,
3922
- data: this.buildShieldCalldata(params)
4075
+ data: buildClaimShieldedFeesCalldata(params)
3923
4076
  });
3924
4077
  }
3925
- /**
3926
- * Estimates the EVM gas for a `privateTransfer` call.
3927
- */
4078
+ // ─── Gas estimation ──────────────────────────────────────────────────────
4079
+ //
4080
+ // `from` must be the real sender: the precompile resolves it to an
4081
+ // AccountId32, so estimating from a different address measures a different
4082
+ // call.
4083
+ async estimateShieldGas(params, from) {
4084
+ return this.evm.estimateGas({ from, to: this.addr, data: buildShieldCalldata(params) });
4085
+ }
3928
4086
  async estimatePrivateTransferGas(params, from) {
3929
4087
  return this.evm.estimateGas({
3930
4088
  from,
3931
4089
  to: this.addr,
3932
- data: this.buildPrivateTransferCalldata(params)
4090
+ data: buildPrivateTransferCalldata(params)
3933
4091
  });
3934
4092
  }
3935
- /**
3936
- * Estimates the EVM gas for an `unshield` call.
3937
- */
3938
4093
  async estimateUnshieldGas(params, from) {
3939
- return this.evm.estimateGas({
3940
- from,
3941
- to: this.addr,
3942
- data: this.buildUnshieldCalldata(params)
3943
- });
3944
- }
3945
- // ─── claimShieldedFees ───────────────────────────────────────────────────────────────────
3946
- /**
3947
- * Returns the ABI-encoded calldata for
3948
- * `claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes,uint32)`.
3949
- *
3950
- * ABI layout (params after selector):
3951
- * - `commitment` — bytes32 (fixed)
3952
- * - `amount` — uint256 (fixed)
3953
- * - `asset_id` — uint32 (fixed, right-aligned)
3954
- * - `memo` — bytes (dynamic)
3955
- * - `proof` — bytes (dynamic, 128 bytes Groth16)
3956
- * - `publicSignals` — bytes (dynamic, 76 bytes)
3957
- * - `circuitVersion` — uint32 (fixed, right-aligned)
3958
- *
3959
- * The validator identity is derived from `msg.sender` in the precompile —
3960
- * do NOT include it in the calldata.
3961
- */
3962
- buildClaimShieldedFeesCalldata(params) {
3963
- EncryptedMemo.validate(
3964
- params.encryptedMemo,
3965
- "buildClaimShieldedFeesCalldata.encryptedMemo"
3966
- );
3967
- if (params.proof.length === 0) {
3968
- throw new Error("claimShieldedFees: proof must not be empty");
3969
- }
3970
- if (params.publicSignals.length !== 76) {
3971
- throw new Error(
3972
- `claimShieldedFees: publicSignals must be 76 bytes, got ${params.publicSignals.length}`
3973
- );
3974
- }
3975
- const commitment = fromHex(params.commitment);
3976
- return encodeHex(
3977
- SP_SEL.CLAIM_SHIELDED_FEES,
3978
- { type: "bytes32", value: commitment },
3979
- { type: "uint", value: params.amount },
3980
- { type: "uint", value: BigInt(params.assetId) },
3981
- { type: "bytes", value: params.encryptedMemo },
3982
- { type: "bytes", value: params.proof },
3983
- { type: "bytes", value: params.publicSignals },
3984
- { type: "uint", value: BigInt(params.circuitVersion) }
3985
- );
3986
- }
3987
- /**
3988
- * Claims accumulated relay fees as a private shielded note.
3989
- *
3990
- * This extrinsic is for **validators/relayers** who have accrued fees in
3991
- * `pallet-relayer` and want to receive them privately inside the shielded pool
3992
- * instead of as a public balance credit.
3993
- *
3994
- * The ZK `value_proof` binds `commitment` to `(amount, assetId, ownerPk, blinding)`
3995
- * so the runtime can verify the note encodes exactly the claimed fee amount,
3996
- * preventing a malicious relayer from inflating the withdrawal.
3997
- *
3998
- * The `msg.sender` EVM address is used as the validator identity; it must match
3999
- * the address that has pending relay fees in `pallet-relayer`.
4000
- *
4001
- * Extrinsic: `shieldedPool.claim_shielded_fees(commitment, amount, assetId, memo, proof, publicSignals)`
4002
- */
4003
- async claimShieldedFees(params, signer) {
4004
- return signer({
4005
- to: this.addr,
4006
- data: this.buildClaimShieldedFeesCalldata(params)
4007
- });
4094
+ return this.evm.estimateGas({ from, to: this.addr, data: buildUnshieldCalldata(params) });
4008
4095
  }
4009
- /**
4010
- * Estimates the EVM gas for a `claimShieldedFees` call.
4011
- */
4012
4096
  async estimateClaimShieldedFeesGas(params, from) {
4013
4097
  return this.evm.estimateGas({
4014
4098
  from,
4015
4099
  to: this.addr,
4016
- data: this.buildClaimShieldedFeesCalldata(params)
4100
+ data: buildClaimShieldedFeesCalldata(params)
4017
4101
  });
4018
4102
  }
4019
4103
  };
@@ -4185,7 +4269,7 @@ var OrbinumClient = class _OrbinumClient {
4185
4269
  */
4186
4270
  static async connect(config) {
4187
4271
  const substrate = config.papi ? SubstrateClient.adopt(config.papi, config.substrateHttp) : await SubstrateClient.connect(config.substrateWs, config.connectTimeoutMs ?? 15e3);
4188
- const evm = config.evmRpc ? new EvmClient(config.evmRpc) : null;
4272
+ const evm = config.evmRpc ? new EvmClient(config.evmRpc, config.evmRpcPeer) : null;
4189
4273
  return new _OrbinumClient(substrate, evm, config.circuitsBaseUrl);
4190
4274
  }
4191
4275
  /**
@@ -4325,6 +4409,7 @@ var OrbinumClientProvider = class _OrbinumClientProvider {
4325
4409
  connectTimeoutMs: this.connectTimeoutMs
4326
4410
  };
4327
4411
  if (this.config.evmRpc) connectConfig.evmRpc = this.config.evmRpc;
4412
+ if (this.config.evmRpcPeer) connectConfig.evmRpcPeer = this.config.evmRpcPeer;
4328
4413
  if (this.config.circuitsBaseUrl)
4329
4414
  connectConfig.circuitsBaseUrl = this.config.circuitsBaseUrl;
4330
4415
  clientPromise = OrbinumClient.connect(connectConfig);
@@ -5106,6 +5191,9 @@ function methodOf(fnSig) {
5106
5191
  if (fnSig.startsWith("shield(")) return "shield";
5107
5192
  return null;
5108
5193
  }
5194
+ function hasFullHead(data, slots) {
5195
+ return data.length >= slots * 32;
5196
+ }
5109
5197
  function decodePrecompileCalldata(address, input) {
5110
5198
  const info = KNOWN_PRECOMPILES[address.toLowerCase()];
5111
5199
  if (!info || !input || input.length < 10) return null;
@@ -5115,6 +5203,7 @@ function decodePrecompileCalldata(address, input) {
5115
5203
  if (fnSig.startsWith("shield(")) {
5116
5204
  try {
5117
5205
  const data = fromHex(input.slice(10));
5206
+ if (!hasFullHead(data, 3)) return { fnSig, method: methodOf(fnSig), args: {} };
5118
5207
  const assetId = decodeUint(data, 0);
5119
5208
  const commitment = toHex(data.slice(32, 64));
5120
5209
  return { fnSig, method: methodOf(fnSig), args: { assetId, commitment } };
@@ -5125,6 +5214,7 @@ function decodePrecompileCalldata(address, input) {
5125
5214
  if (fnSig.startsWith("unshield(")) {
5126
5215
  try {
5127
5216
  const data = fromHex(input.slice(10));
5217
+ if (!hasFullHead(data, 10)) return { fnSig, method: methodOf(fnSig), args: {} };
5128
5218
  const root = toHex(data.slice(32, 64));
5129
5219
  const nullifier = toHex(data.slice(64, 96));
5130
5220
  const assetId = decodeUint(data, 96);
@@ -5154,18 +5244,25 @@ function decodePrecompileCalldata(address, input) {
5154
5244
  if (fnSig.startsWith("privateTransfer(")) {
5155
5245
  try {
5156
5246
  const data = fromHex(input.slice(10));
5247
+ if (!hasFullHead(data, 8)) return { fnSig, method: methodOf(fnSig), args: {} };
5157
5248
  const root = toHex(data.slice(32, 64));
5158
- const nullOffset = Number(decodeUint(data, 64));
5159
- const commOffset = Number(decodeUint(data, 96));
5160
- const nullifiers = Number(decodeUint(data, nullOffset));
5161
- const commitments = Number(decodeUint(data, commOffset));
5162
5249
  const assetId = decodeUint(data, 160);
5163
5250
  const fee = decodeUint(data, 192);
5164
5251
  const circuitVersion = decodeUint(data, 224);
5252
+ const counts = {};
5253
+ for (const [name, slot] of [
5254
+ ["nullifiers", 64],
5255
+ ["commitments", 96]
5256
+ ]) {
5257
+ const offset = decodeUint(data, slot);
5258
+ if (offset <= BigInt(data.length - 32)) {
5259
+ counts[name] = Number(decodeUint(data, Number(offset)));
5260
+ }
5261
+ }
5165
5262
  return {
5166
5263
  fnSig,
5167
5264
  method: methodOf(fnSig),
5168
- args: { root, nullifiers, commitments, assetId, fee, circuitVersion }
5265
+ args: { root, ...counts, assetId, fee, circuitVersion }
5169
5266
  };
5170
5267
  } catch {
5171
5268
  return { fnSig, method: methodOf(fnSig), args: {} };
@@ -5174,6 +5271,7 @@ function decodePrecompileCalldata(address, input) {
5174
5271
  if (fnSig.startsWith("claimShieldedFees(")) {
5175
5272
  try {
5176
5273
  const data = fromHex(input.slice(10));
5274
+ if (!hasFullHead(data, 7)) return { fnSig, method: methodOf(fnSig), args: {} };
5177
5275
  const commitment = toHex(data.slice(0, 32));
5178
5276
  const amount = decodeUint(data, 32);
5179
5277
  const assetId = decodeUint(data, 64);
@@ -5380,7 +5478,7 @@ var MemoryVaultStorage = class {
5380
5478
  };
5381
5479
 
5382
5480
  // src/wallet/vault/storage/config.ts
5383
- var VAULT_SCHEMA_VERSION = 4;
5481
+ var VAULT_SCHEMA_VERSION = 5;
5384
5482
  function normalizeChainFingerprint(chainFingerprint) {
5385
5483
  return chainFingerprint ? chainFingerprint.toLowerCase() : void 0;
5386
5484
  }
@@ -5529,7 +5627,7 @@ async function noteBlindTag(blindKey, hex) {
5529
5627
 
5530
5628
  // src/wallet/vault/notes/meta.ts
5531
5629
  function noteOrigin(note) {
5532
- return note.counterpartyPk === 0n ? "shield" : "private-transfer";
5630
+ return note.sourcePk === 0n ? "shield" : "private-transfer";
5533
5631
  }
5534
5632
  function noteCreatedAt(note) {
5535
5633
  return note.createdAt ?? null;
@@ -5598,9 +5696,9 @@ var NOTE_BIGINT_FIELDS = [
5598
5696
  "spendingKey",
5599
5697
  "commitment",
5600
5698
  "nullifier",
5601
- "counterpartyPk"
5699
+ "sourcePk"
5602
5700
  ];
5603
- var ABSENT_MEANS_ZERO = /* @__PURE__ */ new Set(["counterpartyPk"]);
5701
+ var ABSENT_MEANS_ZERO = /* @__PURE__ */ new Set(["sourcePk"]);
5604
5702
  function normalizeNote(note) {
5605
5703
  let patch = null;
5606
5704
  for (const field of NOTE_BIGINT_FIELDS) {
@@ -5853,12 +5951,12 @@ var VaultStore = class {
5853
5951
  const { blindKey } = this.keys();
5854
5952
  if (commitmentHexes.length === 0) return 0;
5855
5953
  const toRemove = new Set(commitmentHexes);
5856
- const present = this.deps.notes.get().filter((n) => toRemove.has(n.commitmentHex));
5857
- if (present.length === 0) return 0;
5858
- const tags = await Promise.all(present.map((n) => noteBlindTag(blindKey, n.commitmentHex)));
5954
+ const present2 = this.deps.notes.get().filter((n) => toRemove.has(n.commitmentHex));
5955
+ if (present2.length === 0) return 0;
5956
+ const tags = await Promise.all(present2.map((n) => noteBlindTag(blindKey, n.commitmentHex)));
5859
5957
  await this.deps.storage.deleteNotes(tags);
5860
5958
  this.deps.notes.set(removeByCommitment(this.deps.notes.get(), toRemove));
5861
- return present.length;
5959
+ return present2.length;
5862
5960
  }
5863
5961
  /**
5864
5962
  * Stores an outgoing transaction, encrypted.
@@ -6460,26 +6558,147 @@ function parseFeeArg(argsJson) {
6460
6558
  }
6461
6559
  }
6462
6560
 
6561
+ // src/wallet/provenance/selectDescribingNote.ts
6562
+ function hasSourcePk(note) {
6563
+ return typeof note.sourcePk === "bigint" && note.sourcePk !== 0n;
6564
+ }
6565
+ function selectDescribingNote(candidates) {
6566
+ return candidates.find(hasSourcePk) ?? candidates[0];
6567
+ }
6568
+ function selectDescribingNoteByCommitment(commitments, noteByCommitment) {
6569
+ const owned = commitments.map((hex) => noteByCommitment.get(hex)).filter((note) => note !== void 0);
6570
+ return selectDescribingNote(owned);
6571
+ }
6572
+
6573
+ // src/wallet/provenance/merge.ts
6574
+ var SOURCE_RANK = {
6575
+ witnessed: 3,
6576
+ memo: 2,
6577
+ chain: 1,
6578
+ inferred: 0
6579
+ };
6580
+ function rankOf(source) {
6581
+ return SOURCE_RANK[source] ?? -1;
6582
+ }
6583
+ function outranks(a, b) {
6584
+ return rankOf(a) > rankOf(b);
6585
+ }
6586
+ function mergeProvenance(existing, incoming) {
6587
+ if (existing.id !== incoming.id) {
6588
+ throw new Error(
6589
+ `mergeProvenance: id mismatch \u2014 refusing to merge ${existing.id} with ${incoming.id}`
6590
+ );
6591
+ }
6592
+ const incomingWins = outranks(incoming.source, existing.source);
6593
+ const base = incomingWins ? incoming : existing;
6594
+ const other = incomingWins ? existing : incoming;
6595
+ const fee = base.feePlanck ?? other.feePlanck;
6596
+ const publicRecipient = firstPresent(base.publicRecipient, other.publicRecipient);
6597
+ const slip = present(base.slip?.encoded) ? base.slip : other.slip ?? base.slip;
6598
+ const note = base.note ?? other.note;
6599
+ return {
6600
+ // The loser is spread FIRST so a field only it carries survives. A host
6601
+ // stores its own record type through this — `ReconstructedTxRecord` has
6602
+ // `amountApproximate`, which marks an amount derived without subtracting
6603
+ // the fee — and dropping such a field turns an approximate figure into
6604
+ // one that merely looks exact. Every key the winner knows about is
6605
+ // overwritten below, so rank still decides every shared fact.
6606
+ ...other,
6607
+ ...base,
6608
+ // A known peer beats an unknown one even when the winner is silent:
6609
+ // backfilling the recipient is the whole point of re-running this.
6610
+ // `scope: 'none'` is "this operation has no counterparty", so it is a
6611
+ // gap too — treating it as known would block the backfill it exists for.
6612
+ peer: copyPeer(knownPeer(base.peer) ?? knownPeer(other.peer) ?? base.peer ?? other.peer),
6613
+ // A number that stands for "not known yet" must not win by rank. Zero is
6614
+ // exactly that here: `RECOVERED_TX_RESULT` and a failed submission both
6615
+ // report block 0, and their own comment says a caller who needs the
6616
+ // value must look it up rather than trust the field.
6617
+ blockNumber: firstPositive(base.blockNumber, other.blockNumber),
6618
+ timestampMs: firstPositive(base.timestampMs, other.timestampMs),
6619
+ // Reconstruction writes an empty hash when the extrinsic was not decoded.
6620
+ hash: firstPresent(base.hash, other.hash) ?? base.hash,
6621
+ // `exact` is a property of the FIGURE, not of the source. A `witnessed`
6622
+ // row whose amount was marked approximate is not better data than an
6623
+ // exact one from a weaker source, so rank only breaks a tie between two
6624
+ // figures of equal standing.
6625
+ amount: { ...betterAmount(base.amount, other.amount) },
6626
+ // The chain's outcome is not something one source knows better than
6627
+ // another — anyone who looks sees the same thing. Letting rank decide
6628
+ // would let a row written at submit time mark a transaction failed that
6629
+ // the chain went on to accept.
6630
+ status: base.status === "success" || other.status === "success" ? "success" : "failed",
6631
+ ...fee !== void 0 && { feePlanck: fee },
6632
+ ...publicRecipient !== void 0 && { publicRecipient },
6633
+ ...slip !== void 0 && { slip: { ...slip } },
6634
+ ...note !== void 0 && { note: { ...note } }
6635
+ };
6636
+ }
6637
+ function betterAmount(base, other) {
6638
+ if (base.exact === other.exact) return base;
6639
+ return base.exact ? base : other;
6640
+ }
6641
+ function present(value) {
6642
+ return value !== void 0 && value.length > 0;
6643
+ }
6644
+ function firstPresent(base, other) {
6645
+ return present(base) ? base : present(other) ? other : void 0;
6646
+ }
6647
+ function firstPositive(base, other) {
6648
+ return base > 0 ? base : other > 0 ? other : base;
6649
+ }
6650
+ function copyPeer(peer) {
6651
+ return peer ? { ...peer } : null;
6652
+ }
6653
+ function knownPeer(peer) {
6654
+ return peer && peer.scope !== "none" ? peer : null;
6655
+ }
6656
+
6657
+ // src/wallet/provenance/regenerateSlip.ts
6658
+ function regeneratePaymentSlip(facts, recipientIvkPacked, txHash) {
6659
+ if (!isHexOfLength(facts.commitmentHex, 32)) {
6660
+ throw new Error("regeneratePaymentSlip: commitmentHex must be 32 bytes of hex");
6661
+ }
6662
+ if (!isHexOfLength(facts.encryptedMemo, ENCRYPTED_MEMO_SIZE)) {
6663
+ throw new Error(
6664
+ `regeneratePaymentSlip: encryptedMemo must be ${ENCRYPTED_MEMO_SIZE} bytes of hex`
6665
+ );
6666
+ }
6667
+ if (facts.leafIndex !== void 0 && !isValidLeafIndex(facts.leafIndex)) {
6668
+ throw new Error("regeneratePaymentSlip: leafIndex must be a real tree position");
6669
+ }
6670
+ const envelope = sealPaymentSlip(recipientIvkPacked, {
6671
+ commitmentHex: facts.commitmentHex,
6672
+ encryptedMemo: facts.encryptedMemo,
6673
+ ...facts.leafIndex !== void 0 ? { leafIndex: facts.leafIndex } : {},
6674
+ // Rendered as an explorer link by the recipient, where an unconstrained
6675
+ // 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
+ ...isHexOfLength(txHash, 32) ? { txHash } : {}
6679
+ });
6680
+ return encodePaymentSlip(envelope);
6681
+ }
6682
+
6463
6683
  // src/wallet/scanner/history/reconstruct.ts
6464
6684
  var ZERO_PK = "0x" + "00".repeat(32);
6465
6685
  function extrinsicKey(row) {
6466
6686
  return `${row.blockNumber}:${row.extrinsicIndex ?? "null"}`;
6467
6687
  }
6468
- function toPkHex(counterpartyPk) {
6469
- return counterpartyPk != null && counterpartyPk !== 0n ? scalarToHex(counterpartyPk) : ZERO_PK;
6470
- }
6471
- function findChangeNote(commitments, noteByCommitment) {
6472
- const candidates = commitments.map((h) => noteByCommitment.get(h)).filter((n) => n !== void 0);
6473
- return candidates.find((n) => n.counterpartyPk != null && n.counterpartyPk !== 0n) ?? candidates[0];
6688
+ function toPkHex(sourcePk) {
6689
+ return typeof sourcePk === "bigint" && hasSourcePk({ sourcePk }) ? scalarToHex(sourcePk) : ZERO_PK;
6474
6690
  }
6475
6691
  async function loadExistingRecords(vault) {
6476
6692
  try {
6477
6693
  const records = await vault.getTxRecords();
6478
- return new Map(records.map((r) => [r.hash, r]));
6694
+ return new Map(records.map((r) => [r.id, r]));
6479
6695
  } catch {
6480
6696
  return /* @__PURE__ */ new Map();
6481
6697
  }
6482
6698
  }
6699
+ function recordKey(transfer) {
6700
+ return transfer.hash ?? `${transfer.blockNumber}-${transfer.extrinsicIndex ?? 0}`;
6701
+ }
6483
6702
  async function reconstructOutgoingTxRecords(deps) {
6484
6703
  const { vault, transfers } = deps;
6485
6704
  const now = deps.now ?? Date.now;
@@ -6496,17 +6715,17 @@ async function reconstructOutgoingTxRecords(deps) {
6496
6715
  const commitmentsByExtrinsic = new Map(
6497
6716
  commitmentTransfers.map((ct) => [extrinsicKey(ct), ct.matchedCommitments ?? []])
6498
6717
  );
6499
- const existingByHash = await loadExistingRecords(vault);
6718
+ const existingByKey = await loadExistingRecords(vault);
6500
6719
  for (const transfer of outgoingTransfers) {
6501
- const existing = transfer.hash ? existingByHash.get(transfer.hash) : void 0;
6720
+ const existing = existingByKey.get(recordKey(transfer));
6502
6721
  if (existing?.recipientPkHex && existing.recipientPkHex !== ZERO_PK) continue;
6503
6722
  const inputNotes = (transfer.matchedNullifiers ?? []).map((h) => noteByNullifier.get(h)).filter((n) => n !== void 0);
6504
6723
  if (inputNotes.length === 0) continue;
6505
- const changeNote = findChangeNote(
6724
+ const changeNote = selectDescribingNoteByCommitment(
6506
6725
  commitmentsByExtrinsic.get(extrinsicKey(transfer)) ?? [],
6507
6726
  noteByCommitment
6508
6727
  );
6509
- const recipientPkHex = toPkHex(changeNote?.counterpartyPk);
6728
+ const recipientPkHex = toPkHex(changeNote?.sourcePk);
6510
6729
  if (existing) {
6511
6730
  if (recipientPkHex === ZERO_PK) continue;
6512
6731
  await vault.saveTxRecord({ ...existing, recipientPkHex });
@@ -6517,7 +6736,7 @@ async function reconstructOutgoingTxRecords(deps) {
6517
6736
  const transferAmount = totalInputValue - (changeNote?.value ?? 0n) - (fee ?? 0n);
6518
6737
  if (transferAmount <= 0n) continue;
6519
6738
  const record = {
6520
- id: transfer.hash ?? `${transfer.blockNumber}-${transfer.extrinsicIndex ?? 0}`,
6739
+ id: recordKey(transfer),
6521
6740
  type: "private_transfer",
6522
6741
  blockNumber: transfer.blockNumber,
6523
6742
  hash: transfer.hash ?? "",
@@ -6568,7 +6787,7 @@ async function buildZkNote(params, deps) {
6568
6787
  circuitVersion: params.circuitVersion,
6569
6788
  // Omitted rather than passed as undefined: the builder distinguishes an
6570
6789
  // absent recipient (a self note) from one explicitly set.
6571
- ...params.counterpartyPk !== void 0 && { counterpartyPk: params.counterpartyPk },
6790
+ ...params.sourcePk !== void 0 && { sourcePk: params.sourcePk },
6572
6791
  ...params.recipientOwnerPk !== void 0 && {
6573
6792
  recipientOwnerPk: params.recipientOwnerPk
6574
6793
  },
@@ -6887,7 +7106,7 @@ async function transferNotes(deps, params, onProgress) {
6887
7106
  value: transferAmount,
6888
7107
  assetId: noteA.assetId,
6889
7108
  ownerPk: recipientPk,
6890
- counterpartyPk: effectiveSenderPk,
7109
+ sourcePk: effectiveSenderPk,
6891
7110
  // Undefined → dummy memo; the recipient finds the note by scanning.
6892
7111
  ...recipientViewingPublicKey !== void 0 ? { viewingPublicKey: recipientViewingPublicKey } : {},
6893
7112
  // With a viewing key present this activates stealth derivation.
@@ -6898,7 +7117,7 @@ async function transferNotes(deps, params, onProgress) {
6898
7117
  assetId: noteA.assetId,
6899
7118
  ownerPk: effectiveSenderPk,
6900
7119
  spendingKey: noteA.spendingKey,
6901
- counterpartyPk: recipientNote.ownerPk,
7120
+ sourcePk: recipientNote.ownerPk,
6902
7121
  viewingPublicKey: senderViewingPublicKey
6903
7122
  });
6904
7123
  onProgress?.("generating-zk");
@@ -7930,6 +8149,7 @@ var OrbinumWallet = class {
7930
8149
  clearKnownEphWindow,
7931
8150
  clearSession,
7932
8151
  collectNullifiersToQuery,
8152
+ collectOutgoingFacts,
7933
8153
  collectScanEntries,
7934
8154
  commitmentHexOf,
7935
8155
  computeNoteCommitment,
@@ -8005,6 +8225,7 @@ var OrbinumWallet = class {
8005
8225
  getSubstrateSignerFromExtension,
8006
8226
  hasCachedSession,
8007
8227
  hasInjectedExtensions,
8228
+ hasSourcePk,
8008
8229
  hexToBigint,
8009
8230
  hexToNumber,
8010
8231
  implicitSubstrateToEvm,
@@ -8016,6 +8237,7 @@ var OrbinumWallet = class {
8016
8237
  isConnectionLossError,
8017
8238
  isEvmAddress,
8018
8239
  isGhostNoteError,
8240
+ isHexOfLength,
8019
8241
  isImplicitEvmAccount,
8020
8242
  isNativeAsset,
8021
8243
  isNoteSelfConsistent,
@@ -8028,6 +8250,7 @@ var OrbinumWallet = class {
8028
8250
  mapExtrinsicArgs,
8029
8251
  mapZkEventData,
8030
8252
  markInputsSpent,
8253
+ mergeProvenance,
8031
8254
  normalizeChainFingerprint,
8032
8255
  normalizeEvmAddress,
8033
8256
  normalizeNote,
@@ -8042,6 +8265,7 @@ var OrbinumWallet = class {
8042
8265
  noteTxKind,
8043
8266
  openOutgoingBlob,
8044
8267
  openPaymentSlip,
8268
+ outranks,
8045
8269
  pairwiseEphWindow,
8046
8270
  palletErrorKind,
8047
8271
  parseAmount,
@@ -8056,6 +8280,7 @@ var OrbinumWallet = class {
8056
8280
  recoverOwnerPkPoint,
8057
8281
  recoverSelfStealthNote,
8058
8282
  refuseIfAlreadySpent,
8283
+ regeneratePaymentSlip,
8059
8284
  removeByCommitment,
8060
8285
  requireSessionKeys,
8061
8286
  reservePairwiseIndex,
@@ -8069,6 +8294,8 @@ var OrbinumWallet = class {
8069
8294
  scanAbortError,
8070
8295
  sealOutgoingBlob,
8071
8296
  sealPaymentSlip,
8297
+ selectDescribingNote,
8298
+ selectDescribingNoteByCommitment,
8072
8299
  selectGhosts,
8073
8300
  selectNotes,
8074
8301
  selfEphWindow,