@cloak.dev/sdk 0.2.1 → 0.2.2-staging.0f03668

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.cjs CHANGED
@@ -624,12 +624,14 @@ __export(index_exports, {
624
624
  createCloakError: () => createCloakError,
625
625
  createDepositInstruction: () => createDepositInstruction,
626
626
  createLogger: () => createLogger,
627
+ createRecoverableChangeUtxo: () => createRecoverableChangeUtxo,
627
628
  createRecoverableDepositUtxo: () => createRecoverableDepositUtxo,
628
629
  createUtxo: () => createUtxo,
629
630
  createZeroUtxo: () => createZeroUtxo,
630
631
  decryptCompactChainNote: () => decryptCompactChainNote,
631
632
  decryptComplianceMetadataWithMasterKey: () => decryptComplianceMetadataWithMasterKey,
632
633
  decryptTransactionMetadata: () => decryptTransactionMetadata,
634
+ deriveChangeNoteBlinding: () => deriveChangeNoteBlinding,
633
635
  deriveDepositNoteSecrets: () => deriveDepositNoteSecrets,
634
636
  deriveDiversifiedViewingKey: () => deriveDiversifiedViewingKey,
635
637
  deriveDiversifier: () => deriveDiversifier,
@@ -715,6 +717,7 @@ __export(index_exports, {
715
717
  loadPendingDeposits: () => loadPendingDeposits,
716
718
  loadPendingWithdrawals: () => loadPendingWithdrawals,
717
719
  loadVerifiedCircuitArtifacts: () => loadVerifiedCircuitArtifacts,
720
+ matchChangeNote: () => matchChangeNote,
718
721
  matchDepositNote: () => matchDepositNote,
719
722
  matchSwapRefundLeaf: () => matchSwapRefundLeaf,
720
723
  openRecipientDeliveryNote: () => openRecipientDeliveryNote,
@@ -735,6 +738,7 @@ __export(index_exports, {
735
738
  pubkeyToFieldElement: () => pubkeyToFieldElement,
736
739
  pubkeyToLimbs: () => pubkeyToLimbs,
737
740
  randomBytes: () => randomBytes,
741
+ randomChangeNoteSalt: () => randomChangeNoteSalt,
738
742
  randomDepositNoteSalt: () => randomDepositNoteSalt,
739
743
  randomFieldElement: () => randomFieldElement,
740
744
  readMerkleTreeState: () => readMerkleTreeState,
@@ -1387,7 +1391,7 @@ function parseStrictViewingKeyChallenge(response, userPubkey, viewingKeyHex) {
1387
1391
 
1388
1392
  // src/config/relay.ts
1389
1393
  var CLOAK_PRODUCTION_RELAY_URL = "https://api.cloak.ag";
1390
- var RELAY_ORIGIN_ALLOWLIST = [CLOAK_PRODUCTION_RELAY_URL];
1394
+ var RELAY_ORIGIN_ALLOWLIST = ["https://staging-api.cloak.ag"];
1391
1395
  var ABSOLUTE_URL = /^[a-z][a-z0-9+.-]*:\/\//i;
1392
1396
  var LOOPBACK_IPV4 = /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
1393
1397
  var THIS_NETWORK_IPV4 = /^0\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
@@ -5294,6 +5298,93 @@ async function matchDepositNote(params) {
5294
5298
  return { ...secrets, amount, mintAddress, commitment: candidate, noteSalt };
5295
5299
  }
5296
5300
 
5301
+ // src/notes/change-note.ts
5302
+ var import_blake36 = require("@noble/hashes/blake3");
5303
+ init_utxo();
5304
+ init_inputs();
5305
+ var CHANGE_NOTE_DOMAIN = new TextEncoder().encode("cloak_change_note_v1");
5306
+ var CHANGE_BLINDING_INFO = new TextEncoder().encode("blinding");
5307
+ var MAX_OUTPUT_INDEX = 1;
5308
+ var MAX_CHAIN_NOTE_SALT2 = (1n << BigInt(CHAIN_NOTE_SALT_BITS)) - 1n;
5309
+ function saltToBytes2(noteSalt) {
5310
+ if (typeof noteSalt !== "bigint" || noteSalt <= 0n || noteSalt > MAX_CHAIN_NOTE_SALT2) {
5311
+ throw new Error(`noteSalt must be a positive ${CHAIN_NOTE_SALT_BITS}-bit value`);
5312
+ }
5313
+ const out = new Uint8Array(32);
5314
+ let v = noteSalt;
5315
+ for (let i = 31; i >= 0; i--) {
5316
+ out[i] = Number(v & 0xffn);
5317
+ v >>= 8n;
5318
+ }
5319
+ return out;
5320
+ }
5321
+ function toFieldSecret3(bytes) {
5322
+ let value = 0n;
5323
+ for (let i = 0; i < 32; i++) value = value << 8n | BigInt(bytes[i]);
5324
+ const reduced = value % (BN254_MODULUS >> 4n);
5325
+ return reduced === 0n ? 1n : reduced;
5326
+ }
5327
+ function randomChangeNoteSalt() {
5328
+ const bytes = randomBytes(12);
5329
+ let v = 0n;
5330
+ for (const b of bytes) v = v << 8n | BigInt(b);
5331
+ return v === 0n ? 1n : v;
5332
+ }
5333
+ function deriveChangeNoteBlinding(viewingKeyNk, noteSalt, outputIndex) {
5334
+ if (!viewingKeyNk || !(viewingKeyNk instanceof Uint8Array) || viewingKeyNk.length !== 32) {
5335
+ throw new Error("viewingKeyNk must be 32 bytes");
5336
+ }
5337
+ if (!Number.isInteger(outputIndex) || outputIndex < 0 || outputIndex > MAX_OUTPUT_INDEX) {
5338
+ throw new Error(`outputIndex must be 0..${MAX_OUTPUT_INDEX}`);
5339
+ }
5340
+ const salt = saltToBytes2(noteSalt);
5341
+ const preimage = new Uint8Array(
5342
+ CHANGE_NOTE_DOMAIN.length + viewingKeyNk.length + salt.length + 1 + CHANGE_BLINDING_INFO.length
5343
+ );
5344
+ let off = 0;
5345
+ preimage.set(CHANGE_NOTE_DOMAIN, off);
5346
+ off += CHANGE_NOTE_DOMAIN.length;
5347
+ preimage.set(viewingKeyNk, off);
5348
+ off += viewingKeyNk.length;
5349
+ preimage.set(salt, off);
5350
+ off += salt.length;
5351
+ preimage[off] = outputIndex;
5352
+ off += 1;
5353
+ preimage.set(CHANGE_BLINDING_INFO, off);
5354
+ return toFieldSecret3((0, import_blake36.blake3)(preimage));
5355
+ }
5356
+ async function createRecoverableChangeUtxo(amount, keypair, viewingKeyNk, mintAddress = NATIVE_SOL_MINT, noteSalt = randomChangeNoteSalt(), outputIndex = 0) {
5357
+ if (typeof amount !== "bigint" || amount <= 0n) {
5358
+ throw new Error("amount must be a positive bigint");
5359
+ }
5360
+ const blinding = deriveChangeNoteBlinding(viewingKeyNk, noteSalt, outputIndex);
5361
+ const utxo = { amount, keypair, blinding, mintAddress };
5362
+ utxo.commitment = await computeCommitment2(utxo);
5363
+ return { utxo, noteSalt };
5364
+ }
5365
+ function normalizeCommitment2(value) {
5366
+ if (typeof value === "bigint") return value;
5367
+ if (typeof value !== "string") return null;
5368
+ const clean = value.startsWith("0x") ? value.slice(2) : value;
5369
+ if (!/^[0-9a-fA-F]{1,64}$/.test(clean)) return null;
5370
+ return BigInt("0x" + clean);
5371
+ }
5372
+ async function matchChangeNote(params) {
5373
+ const { viewingKeyNk, noteSalt, amount, keypair, mintAddress, outputIndex, outputCommitments } = params;
5374
+ if (typeof amount !== "bigint" || amount <= 0n) return null;
5375
+ if (!keypair || typeof keypair.publicKey !== "bigint" || keypair.publicKey === 0n) return null;
5376
+ let blinding;
5377
+ try {
5378
+ blinding = deriveChangeNoteBlinding(viewingKeyNk, noteSalt, outputIndex);
5379
+ } catch {
5380
+ return null;
5381
+ }
5382
+ const candidate = await computeCommitment2({ amount, keypair, blinding, mintAddress });
5383
+ const published = (outputCommitments ?? []).map(normalizeCommitment2).filter((value) => value !== null);
5384
+ if (!published.some((value) => value === candidate)) return null;
5385
+ return { keypair, blinding, amount, mintAddress, commitment: candidate, noteSalt };
5386
+ }
5387
+
5297
5388
  // src/flows/transact.ts
5298
5389
  var import_circomlibjs4 = require("circomlibjs");
5299
5390
  var import_tweetnacl6 = __toESM(require("tweetnacl"), 1);
@@ -5879,6 +5970,84 @@ async function fetchAltAddressesFromRelayHealth(relayUrl) {
5879
5970
  return [];
5880
5971
  }
5881
5972
  }
5973
+ async function fetchSupplementalAltFromRelay(relayUrl, params) {
5974
+ const body = JSON.stringify({
5975
+ mint: params.mint.toBase58(),
5976
+ nullifiers: [
5977
+ Buffer.from(params.nullifiers[0]).toString("hex"),
5978
+ Buffer.from(params.nullifiers[1]).toString("hex")
5979
+ ],
5980
+ bind0: Buffer.from(params.bind0).toString("hex"),
5981
+ depositor: params.depositor.toBase58()
5982
+ });
5983
+ let response;
5984
+ try {
5985
+ response = await relayFetch(`${relayUrl.replace(/\/$/, "")}/supplemental-alt`, {
5986
+ method: "POST",
5987
+ headers: { "Content-Type": "application/json" },
5988
+ body
5989
+ });
5990
+ } catch (err) {
5991
+ const msg = err instanceof Error ? err.message : String(err);
5992
+ throw new Error(`Supplemental ALT request to the relay failed: ${msg}`);
5993
+ }
5994
+ if (!response.ok) {
5995
+ let text = "";
5996
+ try {
5997
+ text = await response.text();
5998
+ } catch {
5999
+ }
6000
+ throw new Error(
6001
+ `Supplemental ALT request failed (${response.status})${text ? `: ${text}` : ""}`
6002
+ );
6003
+ }
6004
+ let json;
6005
+ try {
6006
+ json = await response.json();
6007
+ } catch {
6008
+ throw new Error("Supplemental ALT response was not valid JSON");
6009
+ }
6010
+ const table = json?.table;
6011
+ if (typeof table !== "string" || table.length === 0) {
6012
+ throw new Error("Supplemental ALT response is missing a 'table' address");
6013
+ }
6014
+ try {
6015
+ return new import_web39.PublicKey(table);
6016
+ } catch {
6017
+ throw new Error(`Supplemental ALT response 'table' is not a valid public key: ${table}`);
6018
+ }
6019
+ }
6020
+ async function resolveRelaySupplementalAlt(connection, table, expected, pollOpts) {
6021
+ const pollIntervalMs = pollOpts?.pollIntervalMs ?? 150;
6022
+ const timeoutMs = pollOpts?.timeoutMs ?? 5e3;
6023
+ const expectedB58 = expected.map((pk) => pk.toBase58());
6024
+ const deadline = Date.now() + timeoutMs;
6025
+ for (; ; ) {
6026
+ const result = await connection.getAddressLookupTable(table, { commitment: "confirmed" });
6027
+ if (!result.value) {
6028
+ throw new Error(`Supplemental ALT ${table.toBase58()} could not be fetched from the RPC.`);
6029
+ }
6030
+ const account = result.value;
6031
+ const present = new Set(account.state.addresses.map((a) => a.toBase58()));
6032
+ const missing = expectedB58.filter((addr) => !present.has(addr));
6033
+ if (missing.length === 0) {
6034
+ const currentSlot = await connection.getSlot("confirmed");
6035
+ if (account.state.lastExtendedSlot < currentSlot) {
6036
+ return account;
6037
+ }
6038
+ if (Date.now() >= deadline) {
6039
+ throw new Error(
6040
+ `Supplemental ALT ${table.toBase58()} did not become resolvable within ${timeoutMs}ms (lastExtendedSlot=${account.state.lastExtendedSlot} never fell behind current slot=${currentSlot}).`
6041
+ );
6042
+ }
6043
+ } else if (Date.now() >= deadline) {
6044
+ throw new Error(
6045
+ `Supplemental ALT ${table.toBase58()} is still missing expected address(es) after ${timeoutMs}ms: ` + missing.join(", ")
6046
+ );
6047
+ }
6048
+ await sleep2(pollIntervalMs);
6049
+ }
6050
+ }
5882
6051
  function deriveRelayUrlFromRiskQuoteUrl(riskQuoteUrl) {
5883
6052
  if (!riskQuoteUrl) return void 0;
5884
6053
  const trimmed = riskQuoteUrl.replace(/\/$/, "");
@@ -6549,7 +6718,7 @@ async function planDirectV0Submission(args) {
6549
6718
  );
6550
6719
  return { tier: "minimal", instructions: minimal, supplementalAltRequested, fullSize, minimalSize };
6551
6720
  }
6552
- async function submitViaExternalFeePayer(connection, depositor, externalFeePayer, externalFeePayerPubkey, instructions, addressLookupTableAccounts, onProgress, allowSupplementalAlt = true) {
6721
+ async function submitViaExternalFeePayer(connection, depositor, externalFeePayer, externalFeePayerPubkey, instructions, addressLookupTableAccounts, onProgress, allowSupplementalAlt = true, relayAltAttempt) {
6553
6722
  if (!depositor.signTransaction) {
6554
6723
  throw new Error(
6555
6724
  "externalFeePayer requires wallet-style signTransaction \u2014 keypair-only depositors aren't supported for external fee payment."
@@ -6578,6 +6747,18 @@ async function submitViaExternalFeePayer(connection, depositor, externalFeePayer
6578
6747
  };
6579
6748
  const compressWithSupplementalAlt = async (instrs, extraAddresses = []) => {
6580
6749
  onProgress?.("Transaction exceeds packet limit; creating supplemental ALT...");
6750
+ if (relayAltAttempt) {
6751
+ try {
6752
+ onProgress?.("Requesting supplemental lookup table from relay...");
6753
+ addressLookupTableAccounts = [await relayAltAttempt()];
6754
+ return;
6755
+ } catch (err) {
6756
+ const msg = err instanceof Error ? err.message : String(err);
6757
+ onProgress?.(
6758
+ `Relay supplemental ALT failed (${msg}); falling back to a depositor-signed table.`
6759
+ );
6760
+ }
6761
+ }
6581
6762
  try {
6582
6763
  const supplementalAddresses = dedupePubkeys([
6583
6764
  ...collectLookupCandidatesFromInstructions(instrs, externalFeePayerPubkey, void 0),
@@ -6645,7 +6826,7 @@ async function submitViaExternalFeePayer(connection, depositor, externalFeePayer
6645
6826
  }
6646
6827
  return signature;
6647
6828
  }
6648
- async function submitTransactionDirect(connection, programId, depositor, proofBytes, publicInputsBytes, nullifiers, mint, recipient, riskOracleQueue, riskQuoteUrl, getRiskQuoteInstruction, onProgress, addressLookupTableAccounts, rangeApiKey, encryptedNoteBytes, relayUrl, altAddresses, relayer, relayerFee, onTransactProofBuilt, externalFeePayer) {
6829
+ async function submitTransactionDirect(connection, programId, depositor, proofBytes, publicInputsBytes, nullifiers, mint, recipient, riskOracleQueue, riskQuoteUrl, getRiskQuoteInstruction, onProgress, addressLookupTableAccounts, rangeApiKey, encryptedNoteBytes, relayUrl, altAddresses, relayer, relayerFee, onTransactProofBuilt, externalFeePayer, relaySupplementalAlt) {
6649
6830
  onProgress?.("Building transaction...");
6650
6831
  addressLookupTableAccounts = await resolveAddressLookupTableAccounts(
6651
6832
  connection,
@@ -6666,14 +6847,8 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
6666
6847
  "rangeApiKey direct risk quotes do not support H-01 deposit nonce binding. Use relayUrl/riskQuoteUrl or getRiskQuoteInstruction."
6667
6848
  );
6668
6849
  }
6669
- if (isDeposit && (!addressLookupTableAccounts || addressLookupTableAccounts.length === 0) && encryptedNoteBytes && encryptedNoteBytes.length > 0) {
6670
- onProgress?.("Creating address lookup table (chain notes require v0 tx to fit)...");
6671
- addressLookupTableAccounts = await createEphemeralALT(connection, depositor, onProgress, [], externalFeePayer);
6672
- }
6673
- if (isDeposit && isSplPool && (!addressLookupTableAccounts || addressLookupTableAccounts.length === 0)) {
6674
- onProgress?.("Creating address lookup table (SPL deposit requires v0 tx)...");
6675
- addressLookupTableAccounts = await createEphemeralALT(connection, depositor, onProgress, [], externalFeePayer);
6676
- }
6850
+ const needsAltForChainNotes = isDeposit && Boolean(encryptedNoteBytes && encryptedNoteBytes.length > 0);
6851
+ const needsAltForSplDeposit = isDeposit && isSplPool;
6677
6852
  const publicAmountBytes = publicInputsBytes.slice(32, 40);
6678
6853
  const publicAmountBuffer = Buffer.from(publicAmountBytes);
6679
6854
  const publicAmount = readBigInt64LE(publicAmountBuffer, 0);
@@ -6725,6 +6900,49 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
6725
6900
  const nullifier1Hex = Buffer.from(nullifiers[1]).toString("hex");
6726
6901
  let riskNoncePda;
6727
6902
  let depositNonce = null;
6903
+ let relaySupplementalAltPromise;
6904
+ const requestRelaySupplementalAlt = () => {
6905
+ const [nullifierPda0] = deriveNullifierPDA(programId, pdas.pool, nullifiers[0]);
6906
+ const [nullifierPda1] = deriveNullifierPDA(programId, pdas.pool, nullifiers[1]);
6907
+ const [riskNoncePdaForAlt] = deriveRiskNoncePDA(programId, depositNonce);
6908
+ const depositorAta = (0, import_spl_token.getAssociatedTokenAddressSync)(mint, depositor.publicKey, false, import_spl_token.TOKEN_PROGRAM_ID);
6909
+ return fetchSupplementalAltFromRelay(relayUrl, {
6910
+ mint,
6911
+ nullifiers,
6912
+ bind0: depositNonce,
6913
+ depositor: depositor.publicKey
6914
+ }).then(
6915
+ (table) => resolveRelaySupplementalAlt(connection, table, [
6916
+ nullifierPda0,
6917
+ nullifierPda1,
6918
+ riskNoncePdaForAlt,
6919
+ depositorAta
6920
+ ])
6921
+ );
6922
+ };
6923
+ const relayAltEligible = () => Boolean(relaySupplementalAlt && relayUrl && isDeposit && isSplPool && depositNonce && nullifiers);
6924
+ const getRelaySupplementalAlt = () => {
6925
+ if (!relaySupplementalAltPromise) {
6926
+ relaySupplementalAltPromise = requestRelaySupplementalAlt();
6927
+ }
6928
+ return relaySupplementalAltPromise;
6929
+ };
6930
+ const acquireDepositAlt = async () => {
6931
+ if (relayAltEligible()) {
6932
+ try {
6933
+ onProgress?.("Requesting lookup table from relay...");
6934
+ const relayAlt = await getRelaySupplementalAlt();
6935
+ return [relayAlt];
6936
+ } catch (err) {
6937
+ const msg = err instanceof Error ? err.message : String(err);
6938
+ onProgress?.(
6939
+ `Relay lookup table failed (${msg}); falling back to a depositor-signed table.`
6940
+ );
6941
+ }
6942
+ }
6943
+ onProgress?.("Creating address lookup table for V0 transaction...");
6944
+ return createEphemeralALT(connection, depositor, onProgress, [], externalFeePayer);
6945
+ };
6728
6946
  if ((isDeposit || isSend) && riskCheckEnabled && !riskQuoteIx) {
6729
6947
  if (getRiskQuoteInstruction) {
6730
6948
  onProgress?.("Fetching risk quote (custom)...");
@@ -6755,10 +6973,13 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
6755
6973
  }
6756
6974
  if (depositNonce) {
6757
6975
  [riskNoncePda] = deriveRiskNoncePDA(programId, depositNonce);
6976
+ if (relaySupplementalAlt && relayUrl && isSplPool) {
6977
+ getRelaySupplementalAlt().catch(() => {
6978
+ });
6979
+ }
6758
6980
  }
6759
- if (riskQuoteIx && (!addressLookupTableAccounts || addressLookupTableAccounts.length === 0)) {
6760
- onProgress?.("Creating address lookup table for V0 transaction (risk quote)...");
6761
- addressLookupTableAccounts = await createEphemeralALT(connection, depositor, onProgress, [], externalFeePayer);
6981
+ if ((!addressLookupTableAccounts || addressLookupTableAccounts.length === 0) && (riskQuoteIx || needsAltForChainNotes || needsAltForSplDeposit)) {
6982
+ addressLookupTableAccounts = await acquireDepositAlt();
6762
6983
  }
6763
6984
  }
6764
6985
  if (!isDeposit && riskCheckEnabled && recipient && riskQuoteUrl && !riskQuoteIx) {
@@ -6824,7 +7045,14 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
6824
7045
  externalFeePayerPubkey,
6825
7046
  fullInstructions,
6826
7047
  addressLookupTableAccounts,
6827
- onProgress
7048
+ onProgress,
7049
+ void 0,
7050
+ // `addressLookupTableAccounts` above already carries the relay-owned table when eligible
7051
+ // (acquireDepositAlt ran before this branch, same as the non-externalFeePayer path). This is
7052
+ // only for submitViaExternalFeePayer's OWN, separate supplemental round — triggered by the
7053
+ // external payer's extra signature and payment-instruction accounts pushing an already-ALT'd
7054
+ // tx back over the limit — so that round also tries the relay before an ephemeral fallback.
7055
+ relayAltEligible() ? getRelaySupplementalAlt : void 0
6828
7056
  );
6829
7057
  } else if (useV0) {
6830
7058
  const plan = await planDirectV0Submission({
@@ -6833,6 +7061,23 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
6833
7061
  compact: compactInstructions,
6834
7062
  minimal: minimalInstructions,
6835
7063
  createSupplementalAlt: async () => {
7064
+ if (relayAltEligible()) {
7065
+ try {
7066
+ onProgress?.("Requesting supplemental lookup table from relay...");
7067
+ const relayAlt = await getRelaySupplementalAlt();
7068
+ addressLookupTableAccounts = [
7069
+ ...(addressLookupTableAccounts ?? []).filter((t) => !t.key.equals(relayAlt.key)),
7070
+ relayAlt
7071
+ ];
7072
+ ({ blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash());
7073
+ return;
7074
+ } catch (err) {
7075
+ const msg = err instanceof Error ? err.message : String(err);
7076
+ onProgress?.(
7077
+ `Relay supplemental ALT failed (${msg}); falling back to a depositor-signed table.`
7078
+ );
7079
+ }
7080
+ }
6836
7081
  const supplementalAddresses = collectLookupCandidatesFromInstructions(
6837
7082
  minimalInstructions,
6838
7083
  depositor.publicKey,
@@ -7625,6 +7870,21 @@ async function transact(params, options) {
7625
7870
  "options.chainNoteSalt requires an explicit chainNoteViewingKeyNk (or getChainNoteViewingKeyNk). The salt anchors a deposit note derived from that nk; inferring nk from the output note's own key would encrypt the chain note under a different key than the note was derived from, and the deposit would be undiscoverable."
7626
7871
  );
7627
7872
  }
7873
+ const chainNoteSalt = options.chainNoteSalt !== void 0 ? assertChainNoteSalt(options.chainNoteSalt) : randomChainNoteSalt();
7874
+ const chainNoteWillCarrySalt = !options.disableChainNotes && !(options.encryptedNotes && options.encryptedNotes.length > 0);
7875
+ if (explicitChainNoteNk && chainNoteWillCarrySalt) {
7876
+ const inputOwners = new Set(inputUtxos.map((u) => u.keypair.publicKey));
7877
+ for (let i = 0; i < outputUtxos.length; i++) {
7878
+ const out = outputUtxos[i];
7879
+ if (!out || out.amount <= BigInt(0)) continue;
7880
+ if (!inputOwners.has(out.keypair.publicKey)) continue;
7881
+ if (out.blinding !== deriveChangeNoteBlinding(explicitChainNoteNk, chainNoteSalt, i)) {
7882
+ throw new Error(
7883
+ `Output ${i} is change (amount ${out.amount}, owned by an input's keypair) but its blinding is not the one derivable from this wallet's viewing key. It was almost certainly built with \`createUtxo\`, whose blinding is random and is written nowhere on chain \u2014 if the caller ever drops the returned note, the funds are unspendable forever. Build change with \`createRecoverableChangeUtxo(amount, keypair, nk, mint, salt, outputIndex)\` and pass the same salt as \`options.chainNoteSalt\`, or use \`partialWithdraw\` / \`transfer\` / \`swapWithChange\`, which do it for you.`
7884
+ );
7885
+ }
7886
+ }
7887
+ }
7628
7888
  assertAllowedRpcConnection(connection);
7629
7889
  await ensureViewingKeyRegistered(options, onProgress, fallbackNk);
7630
7890
  const resolvedChainNoteNk = explicitChainNoteNk ?? fallbackNk ?? null;
@@ -7979,7 +8239,6 @@ async function transact(params, options) {
7979
8239
  const maxFee = isWithdrawal ? protocolFeeSnapshot : BigInt(0);
7980
8240
  const extDataHash = await computeExtDataHash(recipient ?? null, relayerFee, relayer ?? null, maxFee);
7981
8241
  const chainNoteTimestamp = BigInt(Date.now());
7982
- const chainNoteSalt = options.chainNoteSalt !== void 0 ? assertChainNoteSalt(options.chainNoteSalt) : randomChainNoteSalt();
7983
8242
  const mintField = pubkeyToFieldElement(mint);
7984
8243
  let signature = "pending-" + Date.now().toString();
7985
8244
  let commitmentIndices = [-1, -1];
@@ -8254,7 +8513,8 @@ async function transact(params, options) {
8254
8513
  relayer ?? null,
8255
8514
  relayerFee,
8256
8515
  options.onTransactProofBuilt,
8257
- externalFeePayer
8516
+ externalFeePayer,
8517
+ options.relaySupplementalAlt
8258
8518
  );
8259
8519
  signature = directResult.signature;
8260
8520
  commitmentIndices = directResult.commitmentIndices;
@@ -8646,16 +8906,30 @@ async function transfer(inputUtxos, recipientPubkey, amount, options) {
8646
8906
  const change = inputSum - amount;
8647
8907
  const myKeypair = inputUtxos[0].keypair;
8648
8908
  const mint = inputUtxos[0].mintAddress;
8909
+ const transferNk = await resolveChainNoteNk(options);
8910
+ const changeNoteSalt = transferNk ? randomChangeNoteSalt() : void 0;
8911
+ const isSendToSelf = inputUtxos.some((u) => u.keypair.publicKey === recipientPubkey);
8649
8912
  const recipientKeypair = {
8650
8913
  privateKey: BigInt(0),
8651
8914
  // Recipient's private key unknown
8652
8915
  publicKey: recipientPubkey
8653
8916
  };
8654
- const recipientUtxo = await createUtxo(amount, recipientKeypair, mint);
8917
+ const recipientUtxo = transferNk && isSendToSelf ? (await createRecoverableChangeUtxo(amount, myKeypair, transferNk, mint, changeNoteSalt, 0)).utxo : await createUtxo(amount, recipientKeypair, mint);
8655
8918
  const outputUtxos = [recipientUtxo];
8656
8919
  if (change > BigInt(0)) {
8657
- const changeUtxo = await createUtxo(change, myKeypair, mint);
8658
- outputUtxos.push(changeUtxo);
8920
+ if (transferNk) {
8921
+ const { utxo } = await createRecoverableChangeUtxo(
8922
+ change,
8923
+ myKeypair,
8924
+ transferNk,
8925
+ mint,
8926
+ changeNoteSalt,
8927
+ 1
8928
+ );
8929
+ outputUtxos.push(utxo);
8930
+ } else {
8931
+ outputUtxos.push(await createUtxo(change, myKeypair, mint));
8932
+ }
8659
8933
  }
8660
8934
  return transact(
8661
8935
  {
@@ -8664,7 +8938,7 @@ async function transfer(inputUtxos, recipientPubkey, amount, options) {
8664
8938
  externalAmount: BigInt(0)
8665
8939
  // Pure shield-to-shield
8666
8940
  },
8667
- options
8941
+ changeNoteSalt !== void 0 ? { ...options, chainNoteSalt: changeNoteSalt } : options
8668
8942
  );
8669
8943
  }
8670
8944
  async function partialWithdraw(inputUtxos, recipient, withdrawAmount, options) {
@@ -8681,7 +8955,9 @@ async function partialWithdraw(inputUtxos, recipient, withdrawAmount, options) {
8681
8955
  utxos.sort((a, b) => a.amount > b.amount ? 1 : a.amount < b.amount ? -1 : 0);
8682
8956
  const [small1, small2, ...rest] = utxos;
8683
8957
  const mergedAmount = small1.amount + small2.amount;
8684
- const mergedOutput = await createUtxo(mergedAmount, myKeypair, mint);
8958
+ const mergeNk = await resolveChainNoteNk(options);
8959
+ const mergeSalt = mergeNk ? randomChangeNoteSalt() : void 0;
8960
+ const mergedOutput = mergeNk ? (await createRecoverableChangeUtxo(mergedAmount, myKeypair, mergeNk, mint, mergeSalt, 0)).utxo : await createUtxo(mergedAmount, myKeypair, mint);
8685
8961
  const mergeResult = await transact(
8686
8962
  {
8687
8963
  inputUtxos: [small1, small2],
@@ -8691,7 +8967,8 @@ async function partialWithdraw(inputUtxos, recipient, withdrawAmount, options) {
8691
8967
  },
8692
8968
  {
8693
8969
  ...options,
8694
- cachedMerkleTree: cachedTree
8970
+ cachedMerkleTree: cachedTree,
8971
+ ...mergeSalt !== void 0 && { chainNoteSalt: mergeSalt }
8695
8972
  }
8696
8973
  );
8697
8974
  cachedTree = mergeResult.merkleTree;
@@ -8704,9 +8981,25 @@ async function partialWithdraw(inputUtxos, recipient, withdrawAmount, options) {
8704
8981
  const finalInputSum = sumUtxoAmounts(utxos);
8705
8982
  const change = finalInputSum - withdrawAmount;
8706
8983
  const outputUtxos = [];
8984
+ let changeNoteSalt;
8707
8985
  if (change > BigInt(0)) {
8708
- const changeUtxo = await createUtxo(change, myKeypair, mint);
8709
- outputUtxos.push(changeUtxo);
8986
+ const changeNk = await resolveChainNoteNk(options);
8987
+ if (changeNk) {
8988
+ const salt = randomChangeNoteSalt();
8989
+ const { utxo } = await createRecoverableChangeUtxo(
8990
+ change,
8991
+ myKeypair,
8992
+ changeNk,
8993
+ mint,
8994
+ salt,
8995
+ 0
8996
+ // partialWithdraw puts change at output 0
8997
+ );
8998
+ outputUtxos.push(utxo);
8999
+ changeNoteSalt = salt;
9000
+ } else {
9001
+ outputUtxos.push(await createUtxo(change, myKeypair, mint));
9002
+ }
8710
9003
  }
8711
9004
  return transact(
8712
9005
  {
@@ -8717,7 +9010,8 @@ async function partialWithdraw(inputUtxos, recipient, withdrawAmount, options) {
8717
9010
  },
8718
9011
  {
8719
9012
  ...options,
8720
- cachedMerkleTree: cachedTree
9013
+ cachedMerkleTree: cachedTree,
9014
+ ...changeNoteSalt !== void 0 && { chainNoteSalt: changeNoteSalt }
8721
9015
  }
8722
9016
  );
8723
9017
  }
@@ -8851,7 +9145,16 @@ async function swapUtxo(params, options) {
8851
9145
  onProgress?.("Validating transaction parameters...");
8852
9146
  const swapFallbackNk = getNkFromUtxoPrivateKey(inputUtxos[0].keypair.privateKey);
8853
9147
  await ensureViewingKeyRegistered(options, onProgress, swapFallbackNk);
8854
- const resolvedSwapChainNoteNk = await resolveChainNoteNk(options) ?? swapFallbackNk;
9148
+ const explicitSwapChainNoteNk = await resolveChainNoteNk(options);
9149
+ const resolvedSwapChainNoteNk = explicitSwapChainNoteNk ?? swapFallbackNk;
9150
+ const chainNoteSalt = options.chainNoteSalt !== void 0 ? assertChainNoteSalt(options.chainNoteSalt) : randomChainNoteSalt();
9151
+ if (explicitSwapChainNoteNk && changeUtxo && changeUtxo.amount > BigInt(0) && inputUtxos.some((u) => u.keypair.publicKey === changeUtxo.keypair.publicKey)) {
9152
+ if (changeUtxo.blinding !== deriveChangeNoteBlinding(explicitSwapChainNoteNk, chainNoteSalt, 0)) {
9153
+ throw new Error(
9154
+ `Swap change (amount ${changeUtxo.amount}, owned by an input's keypair) has a blinding that is not derivable from this wallet's viewing key. It was almost certainly built with \`createUtxo\`, whose blinding is random and is written nowhere on chain \u2014 if the caller ever drops the returned note, the funds are unspendable forever. Build it with \`createRecoverableChangeUtxo(amount, keypair, nk, mint, salt, 0)\` and pass the same salt as \`options.chainNoteSalt\`, or use \`swapWithChange\`, which does it for you.`
9155
+ );
9156
+ }
9157
+ }
8855
9158
  const swapPoolMint = NATIVE_SOL_MINT;
8856
9159
  const pdas = getShieldPoolPDAs(programId, swapPoolMint);
8857
9160
  let merkleState = null;
@@ -9193,7 +9496,6 @@ async function swapUtxo(params, options) {
9193
9496
  outputCommitments.push(await computeCommitment2(utxo));
9194
9497
  }
9195
9498
  const chainNoteTimestamp = BigInt(Date.now());
9196
- const chainNoteSalt = randomChainNoteSalt();
9197
9499
  onProgress?.("Computing external data hash...");
9198
9500
  const mint = swapPoolMint;
9199
9501
  const mintField = pubkeyToFieldElement(mint);
@@ -9411,6 +9713,9 @@ async function swapUtxo(params, options) {
9411
9713
  url.searchParams.set("wallet", poolPda.toBase58());
9412
9714
  url.searchParams.set("recipient", recipientWallet.toBase58());
9413
9715
  url.searchParams.set("sender", recipientWallet.toBase58());
9716
+ url.searchParams.set("pool_mint", mint.toBase58());
9717
+ url.searchParams.set("nullifier0", toHex(bigintToBytes323(inputNullifiers[0])));
9718
+ url.searchParams.set("nullifier1", toHex(bigintToBytes323(inputNullifiers[1])));
9414
9719
  const quoteRes = await relayFetch(url.toString());
9415
9720
  if (!quoteRes.ok) {
9416
9721
  const text = await quoteRes.text();
@@ -9655,10 +9960,18 @@ async function swapWithChange(inputUtxos, swapAmount, outputMint, recipientAta,
9655
9960
  throw new Error(`Insufficient balance: input(${totalInput}) < swap(${swapAmount})`);
9656
9961
  }
9657
9962
  let changeUtxo;
9963
+ let changeNoteSalt;
9658
9964
  if (change > BigInt(0)) {
9659
9965
  const myKeypair = inputUtxos[0].keypair;
9660
9966
  const mint = inputUtxos[0].mintAddress;
9661
- changeUtxo = await createUtxo(change, myKeypair, mint);
9967
+ const changeNk = await resolveChainNoteNk(options);
9968
+ if (changeNk) {
9969
+ const salt = randomChangeNoteSalt();
9970
+ changeUtxo = (await createRecoverableChangeUtxo(change, myKeypair, changeNk, mint, salt, 0)).utxo;
9971
+ changeNoteSalt = salt;
9972
+ } else {
9973
+ changeUtxo = await createUtxo(change, myKeypair, mint);
9974
+ }
9662
9975
  }
9663
9976
  return swapUtxo(
9664
9977
  {
@@ -9670,7 +9983,7 @@ async function swapWithChange(inputUtxos, swapAmount, outputMint, recipientAta,
9670
9983
  recipientWallet,
9671
9984
  minOutputAmount
9672
9985
  },
9673
- options
9986
+ changeNoteSalt !== void 0 ? { ...options, chainNoteSalt: changeNoteSalt } : options
9674
9987
  );
9675
9988
  }
9676
9989
 
@@ -10476,6 +10789,7 @@ async function scanTransactions(opts) {
10476
10789
  const txs = [];
10477
10790
  const swapCtxByCommitment = /* @__PURE__ */ new Map();
10478
10791
  const recoveredDepositNotes = [];
10792
+ const recoveredChangeNotes = [];
10479
10793
  let processed = 0;
10480
10794
  let rpcCallsMade = 0;
10481
10795
  onStatus?.(`Scanning ${sigInfos.length} transactions...`);
@@ -10823,6 +11137,34 @@ async function scanTransactions(opts) {
10823
11137
  if (debug) console.log(`[DEBUG] ${sigInfo.signature}: deposit recovery failed (${e?.message || e})`);
10824
11138
  }
10825
11139
  }
11140
+ if (isWithdrawal && compactNote.noteSalt !== void 0 && compactNote.outAmount0 !== void 0 && compactNote.outAmount0 > 0n && compactNote.outPubkey0 !== void 0 && compactNote.outPubkey0 !== 0n) {
11141
+ try {
11142
+ const recoveredChange = await matchChangeNote({
11143
+ viewingKeyNk,
11144
+ noteSalt: compactNote.noteSalt,
11145
+ amount: compactNote.outAmount0,
11146
+ keypair: { privateKey: 0n, publicKey: compactNote.outPubkey0 },
11147
+ mintAddress: new import_web310.PublicKey(asset.mint),
11148
+ outputIndex: 0,
11149
+ // v4 describes output 0, which is where change lands
11150
+ outputCommitments: ixCtx.outputCommitments ?? []
11151
+ });
11152
+ if (recoveredChange) {
11153
+ recoveredChangeNotes.push({
11154
+ ...recoveredChange,
11155
+ signature: sigInfo.signature,
11156
+ timestamp: decoded.timestamp
11157
+ });
11158
+ if (debug) {
11159
+ console.log(
11160
+ `[DEBUG] ${sigInfo.signature}: recovered change note (amount=${recoveredChange.amount})`
11161
+ );
11162
+ }
11163
+ }
11164
+ } catch (e) {
11165
+ if (debug) console.log(`[DEBUG] ${sigInfo.signature}: change recovery failed (${e?.message || e})`);
11166
+ }
11167
+ }
10826
11168
  txs.push({
10827
11169
  txType: decoded.txType,
10828
11170
  amount: grossAmount,
@@ -10928,7 +11270,8 @@ async function scanTransactions(opts) {
10928
11270
  lastSignature: newestSignature,
10929
11271
  rpcCallsMade,
10930
11272
  deliveredNotes,
10931
- recoveredDepositNotes
11273
+ recoveredDepositNotes,
11274
+ recoveredChangeNotes
10932
11275
  };
10933
11276
  }
10934
11277
  function toComplianceReport(result) {
@@ -11376,12 +11719,14 @@ var SCANNER_SUPPORTS_TRANSACT_SWAP = true;
11376
11719
  createCloakError,
11377
11720
  createDepositInstruction,
11378
11721
  createLogger,
11722
+ createRecoverableChangeUtxo,
11379
11723
  createRecoverableDepositUtxo,
11380
11724
  createUtxo,
11381
11725
  createZeroUtxo,
11382
11726
  decryptCompactChainNote,
11383
11727
  decryptComplianceMetadataWithMasterKey,
11384
11728
  decryptTransactionMetadata,
11729
+ deriveChangeNoteBlinding,
11385
11730
  deriveDepositNoteSecrets,
11386
11731
  deriveDiversifiedViewingKey,
11387
11732
  deriveDiversifier,
@@ -11467,6 +11812,7 @@ var SCANNER_SUPPORTS_TRANSACT_SWAP = true;
11467
11812
  loadPendingDeposits,
11468
11813
  loadPendingWithdrawals,
11469
11814
  loadVerifiedCircuitArtifacts,
11815
+ matchChangeNote,
11470
11816
  matchDepositNote,
11471
11817
  matchSwapRefundLeaf,
11472
11818
  openRecipientDeliveryNote,
@@ -11487,6 +11833,7 @@ var SCANNER_SUPPORTS_TRANSACT_SWAP = true;
11487
11833
  pubkeyToFieldElement,
11488
11834
  pubkeyToLimbs,
11489
11835
  randomBytes,
11836
+ randomChangeNoteSalt,
11490
11837
  randomDepositNoteSalt,
11491
11838
  randomFieldElement,
11492
11839
  readMerkleTreeState,