@cloak.dev/sdk 0.2.1 → 0.2.2-staging.33f11a2

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
@@ -621,7 +621,7 @@ function parseStrictViewingKeyChallenge(response, userPubkey, viewingKeyHex) {
621
621
 
622
622
  // src/config/relay.ts
623
623
  var CLOAK_PRODUCTION_RELAY_URL = "https://api.cloak.ag";
624
- var RELAY_ORIGIN_ALLOWLIST = [CLOAK_PRODUCTION_RELAY_URL];
624
+ var RELAY_ORIGIN_ALLOWLIST = ["https://staging-api.cloak.ag"];
625
625
  var ABSOLUTE_URL = /^[a-z][a-z0-9+.-]*:\/\//i;
626
626
  var LOOPBACK_IPV4 = /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
627
627
  var THIS_NETWORK_IPV4 = /^0\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
@@ -4533,6 +4533,91 @@ async function matchDepositNote(params) {
4533
4533
  return { ...secrets, amount, mintAddress, commitment: candidate, noteSalt };
4534
4534
  }
4535
4535
 
4536
+ // src/notes/change-note.ts
4537
+ import { blake3 as blake35 } from "@noble/hashes/blake3";
4538
+ var CHANGE_NOTE_DOMAIN = new TextEncoder().encode("cloak_change_note_v1");
4539
+ var CHANGE_BLINDING_INFO = new TextEncoder().encode("blinding");
4540
+ var MAX_OUTPUT_INDEX = 1;
4541
+ var MAX_CHAIN_NOTE_SALT2 = (1n << BigInt(CHAIN_NOTE_SALT_BITS)) - 1n;
4542
+ function saltToBytes2(noteSalt) {
4543
+ if (typeof noteSalt !== "bigint" || noteSalt <= 0n || noteSalt > MAX_CHAIN_NOTE_SALT2) {
4544
+ throw new Error(`noteSalt must be a positive ${CHAIN_NOTE_SALT_BITS}-bit value`);
4545
+ }
4546
+ const out = new Uint8Array(32);
4547
+ let v = noteSalt;
4548
+ for (let i = 31; i >= 0; i--) {
4549
+ out[i] = Number(v & 0xffn);
4550
+ v >>= 8n;
4551
+ }
4552
+ return out;
4553
+ }
4554
+ function toFieldSecret3(bytes) {
4555
+ let value = 0n;
4556
+ for (let i = 0; i < 32; i++) value = value << 8n | BigInt(bytes[i]);
4557
+ const reduced = value % (BN254_MODULUS >> 4n);
4558
+ return reduced === 0n ? 1n : reduced;
4559
+ }
4560
+ function randomChangeNoteSalt() {
4561
+ const bytes = randomBytes(12);
4562
+ let v = 0n;
4563
+ for (const b of bytes) v = v << 8n | BigInt(b);
4564
+ return v === 0n ? 1n : v;
4565
+ }
4566
+ function deriveChangeNoteBlinding(viewingKeyNk, noteSalt, outputIndex) {
4567
+ if (!viewingKeyNk || !(viewingKeyNk instanceof Uint8Array) || viewingKeyNk.length !== 32) {
4568
+ throw new Error("viewingKeyNk must be 32 bytes");
4569
+ }
4570
+ if (!Number.isInteger(outputIndex) || outputIndex < 0 || outputIndex > MAX_OUTPUT_INDEX) {
4571
+ throw new Error(`outputIndex must be 0..${MAX_OUTPUT_INDEX}`);
4572
+ }
4573
+ const salt = saltToBytes2(noteSalt);
4574
+ const preimage = new Uint8Array(
4575
+ CHANGE_NOTE_DOMAIN.length + viewingKeyNk.length + salt.length + 1 + CHANGE_BLINDING_INFO.length
4576
+ );
4577
+ let off = 0;
4578
+ preimage.set(CHANGE_NOTE_DOMAIN, off);
4579
+ off += CHANGE_NOTE_DOMAIN.length;
4580
+ preimage.set(viewingKeyNk, off);
4581
+ off += viewingKeyNk.length;
4582
+ preimage.set(salt, off);
4583
+ off += salt.length;
4584
+ preimage[off] = outputIndex;
4585
+ off += 1;
4586
+ preimage.set(CHANGE_BLINDING_INFO, off);
4587
+ return toFieldSecret3(blake35(preimage));
4588
+ }
4589
+ async function createRecoverableChangeUtxo(amount, keypair, viewingKeyNk, mintAddress = NATIVE_SOL_MINT, noteSalt = randomChangeNoteSalt(), outputIndex = 0) {
4590
+ if (typeof amount !== "bigint" || amount <= 0n) {
4591
+ throw new Error("amount must be a positive bigint");
4592
+ }
4593
+ const blinding = deriveChangeNoteBlinding(viewingKeyNk, noteSalt, outputIndex);
4594
+ const utxo = { amount, keypair, blinding, mintAddress };
4595
+ utxo.commitment = await computeCommitment(utxo);
4596
+ return { utxo, noteSalt };
4597
+ }
4598
+ function normalizeCommitment2(value) {
4599
+ if (typeof value === "bigint") return value;
4600
+ if (typeof value !== "string") return null;
4601
+ const clean = value.startsWith("0x") ? value.slice(2) : value;
4602
+ if (!/^[0-9a-fA-F]{1,64}$/.test(clean)) return null;
4603
+ return BigInt("0x" + clean);
4604
+ }
4605
+ async function matchChangeNote(params) {
4606
+ const { viewingKeyNk, noteSalt, amount, keypair, mintAddress, outputIndex, outputCommitments } = params;
4607
+ if (typeof amount !== "bigint" || amount <= 0n) return null;
4608
+ if (!keypair || typeof keypair.publicKey !== "bigint" || keypair.publicKey === 0n) return null;
4609
+ let blinding;
4610
+ try {
4611
+ blinding = deriveChangeNoteBlinding(viewingKeyNk, noteSalt, outputIndex);
4612
+ } catch {
4613
+ return null;
4614
+ }
4615
+ const candidate = await computeCommitment({ amount, keypair, blinding, mintAddress });
4616
+ const published = (outputCommitments ?? []).map(normalizeCommitment2).filter((value) => value !== null);
4617
+ if (!published.some((value) => value === candidate)) return null;
4618
+ return { keypair, blinding, amount, mintAddress, commitment: candidate, noteSalt };
4619
+ }
4620
+
4536
4621
  // src/flows/transact.ts
4537
4622
  import { buildPoseidon as buildPoseidon2 } from "circomlibjs";
4538
4623
  import nacl6 from "tweetnacl";
@@ -5114,6 +5199,84 @@ async function fetchAltAddressesFromRelayHealth(relayUrl) {
5114
5199
  return [];
5115
5200
  }
5116
5201
  }
5202
+ async function fetchSupplementalAltFromRelay(relayUrl, params) {
5203
+ const body = JSON.stringify({
5204
+ mint: params.mint.toBase58(),
5205
+ nullifiers: [
5206
+ Buffer.from(params.nullifiers[0]).toString("hex"),
5207
+ Buffer.from(params.nullifiers[1]).toString("hex")
5208
+ ],
5209
+ bind0: Buffer.from(params.bind0).toString("hex"),
5210
+ depositor: params.depositor.toBase58()
5211
+ });
5212
+ let response;
5213
+ try {
5214
+ response = await relayFetch(`${relayUrl.replace(/\/$/, "")}/supplemental-alt`, {
5215
+ method: "POST",
5216
+ headers: { "Content-Type": "application/json" },
5217
+ body
5218
+ });
5219
+ } catch (err) {
5220
+ const msg = err instanceof Error ? err.message : String(err);
5221
+ throw new Error(`Supplemental ALT request to the relay failed: ${msg}`);
5222
+ }
5223
+ if (!response.ok) {
5224
+ let text = "";
5225
+ try {
5226
+ text = await response.text();
5227
+ } catch {
5228
+ }
5229
+ throw new Error(
5230
+ `Supplemental ALT request failed (${response.status})${text ? `: ${text}` : ""}`
5231
+ );
5232
+ }
5233
+ let json;
5234
+ try {
5235
+ json = await response.json();
5236
+ } catch {
5237
+ throw new Error("Supplemental ALT response was not valid JSON");
5238
+ }
5239
+ const table = json?.table;
5240
+ if (typeof table !== "string" || table.length === 0) {
5241
+ throw new Error("Supplemental ALT response is missing a 'table' address");
5242
+ }
5243
+ try {
5244
+ return new PublicKey8(table);
5245
+ } catch {
5246
+ throw new Error(`Supplemental ALT response 'table' is not a valid public key: ${table}`);
5247
+ }
5248
+ }
5249
+ async function resolveRelaySupplementalAlt(connection, table, expected, pollOpts) {
5250
+ const pollIntervalMs = pollOpts?.pollIntervalMs ?? 150;
5251
+ const timeoutMs = pollOpts?.timeoutMs ?? 5e3;
5252
+ const expectedB58 = expected.map((pk) => pk.toBase58());
5253
+ const deadline = Date.now() + timeoutMs;
5254
+ for (; ; ) {
5255
+ const result = await connection.getAddressLookupTable(table, { commitment: "confirmed" });
5256
+ if (!result.value) {
5257
+ throw new Error(`Supplemental ALT ${table.toBase58()} could not be fetched from the RPC.`);
5258
+ }
5259
+ const account = result.value;
5260
+ const present = new Set(account.state.addresses.map((a) => a.toBase58()));
5261
+ const missing = expectedB58.filter((addr) => !present.has(addr));
5262
+ if (missing.length === 0) {
5263
+ const currentSlot = await connection.getSlot("confirmed");
5264
+ if (account.state.lastExtendedSlot < currentSlot) {
5265
+ return account;
5266
+ }
5267
+ if (Date.now() >= deadline) {
5268
+ throw new Error(
5269
+ `Supplemental ALT ${table.toBase58()} did not become resolvable within ${timeoutMs}ms (lastExtendedSlot=${account.state.lastExtendedSlot} never fell behind current slot=${currentSlot}).`
5270
+ );
5271
+ }
5272
+ } else if (Date.now() >= deadline) {
5273
+ throw new Error(
5274
+ `Supplemental ALT ${table.toBase58()} is still missing expected address(es) after ${timeoutMs}ms: ` + missing.join(", ")
5275
+ );
5276
+ }
5277
+ await sleep2(pollIntervalMs);
5278
+ }
5279
+ }
5117
5280
  function deriveRelayUrlFromRiskQuoteUrl(riskQuoteUrl) {
5118
5281
  if (!riskQuoteUrl) return void 0;
5119
5282
  const trimmed = riskQuoteUrl.replace(/\/$/, "");
@@ -5784,7 +5947,7 @@ async function planDirectV0Submission(args) {
5784
5947
  );
5785
5948
  return { tier: "minimal", instructions: minimal, supplementalAltRequested, fullSize, minimalSize };
5786
5949
  }
5787
- async function submitViaExternalFeePayer(connection, depositor, externalFeePayer, externalFeePayerPubkey, instructions, addressLookupTableAccounts, onProgress, allowSupplementalAlt = true) {
5950
+ async function submitViaExternalFeePayer(connection, depositor, externalFeePayer, externalFeePayerPubkey, instructions, addressLookupTableAccounts, onProgress, allowSupplementalAlt = true, relayAltAttempt) {
5788
5951
  if (!depositor.signTransaction) {
5789
5952
  throw new Error(
5790
5953
  "externalFeePayer requires wallet-style signTransaction \u2014 keypair-only depositors aren't supported for external fee payment."
@@ -5813,6 +5976,18 @@ async function submitViaExternalFeePayer(connection, depositor, externalFeePayer
5813
5976
  };
5814
5977
  const compressWithSupplementalAlt = async (instrs, extraAddresses = []) => {
5815
5978
  onProgress?.("Transaction exceeds packet limit; creating supplemental ALT...");
5979
+ if (relayAltAttempt) {
5980
+ try {
5981
+ onProgress?.("Requesting supplemental lookup table from relay...");
5982
+ addressLookupTableAccounts = [await relayAltAttempt()];
5983
+ return;
5984
+ } catch (err) {
5985
+ const msg = err instanceof Error ? err.message : String(err);
5986
+ onProgress?.(
5987
+ `Relay supplemental ALT failed (${msg}); falling back to a depositor-signed table.`
5988
+ );
5989
+ }
5990
+ }
5816
5991
  try {
5817
5992
  const supplementalAddresses = dedupePubkeys([
5818
5993
  ...collectLookupCandidatesFromInstructions(instrs, externalFeePayerPubkey, void 0),
@@ -5880,7 +6055,7 @@ async function submitViaExternalFeePayer(connection, depositor, externalFeePayer
5880
6055
  }
5881
6056
  return signature;
5882
6057
  }
5883
- async function submitTransactionDirect(connection, programId, depositor, proofBytes, publicInputsBytes, nullifiers, mint, recipient, riskOracleQueue, riskQuoteUrl, getRiskQuoteInstruction, onProgress, addressLookupTableAccounts, rangeApiKey, encryptedNoteBytes, relayUrl, altAddresses, relayer, relayerFee, onTransactProofBuilt, externalFeePayer) {
6058
+ 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) {
5884
6059
  onProgress?.("Building transaction...");
5885
6060
  addressLookupTableAccounts = await resolveAddressLookupTableAccounts(
5886
6061
  connection,
@@ -5901,14 +6076,8 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
5901
6076
  "rangeApiKey direct risk quotes do not support H-01 deposit nonce binding. Use relayUrl/riskQuoteUrl or getRiskQuoteInstruction."
5902
6077
  );
5903
6078
  }
5904
- if (isDeposit && (!addressLookupTableAccounts || addressLookupTableAccounts.length === 0) && encryptedNoteBytes && encryptedNoteBytes.length > 0) {
5905
- onProgress?.("Creating address lookup table (chain notes require v0 tx to fit)...");
5906
- addressLookupTableAccounts = await createEphemeralALT(connection, depositor, onProgress, [], externalFeePayer);
5907
- }
5908
- if (isDeposit && isSplPool && (!addressLookupTableAccounts || addressLookupTableAccounts.length === 0)) {
5909
- onProgress?.("Creating address lookup table (SPL deposit requires v0 tx)...");
5910
- addressLookupTableAccounts = await createEphemeralALT(connection, depositor, onProgress, [], externalFeePayer);
5911
- }
6079
+ const needsAltForChainNotes = isDeposit && Boolean(encryptedNoteBytes && encryptedNoteBytes.length > 0);
6080
+ const needsAltForSplDeposit = isDeposit && isSplPool;
5912
6081
  const publicAmountBytes = publicInputsBytes.slice(32, 40);
5913
6082
  const publicAmountBuffer = Buffer.from(publicAmountBytes);
5914
6083
  const publicAmount = readBigInt64LE(publicAmountBuffer, 0);
@@ -5960,6 +6129,49 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
5960
6129
  const nullifier1Hex = Buffer.from(nullifiers[1]).toString("hex");
5961
6130
  let riskNoncePda;
5962
6131
  let depositNonce = null;
6132
+ let relaySupplementalAltPromise;
6133
+ const requestRelaySupplementalAlt = () => {
6134
+ const [nullifierPda0] = deriveNullifierPDA(programId, pdas.pool, nullifiers[0]);
6135
+ const [nullifierPda1] = deriveNullifierPDA(programId, pdas.pool, nullifiers[1]);
6136
+ const [riskNoncePdaForAlt] = deriveRiskNoncePDA(programId, depositNonce);
6137
+ const depositorAta = getAssociatedTokenAddressSync(mint, depositor.publicKey, false, TOKEN_PROGRAM_ID);
6138
+ return fetchSupplementalAltFromRelay(relayUrl, {
6139
+ mint,
6140
+ nullifiers,
6141
+ bind0: depositNonce,
6142
+ depositor: depositor.publicKey
6143
+ }).then(
6144
+ (table) => resolveRelaySupplementalAlt(connection, table, [
6145
+ nullifierPda0,
6146
+ nullifierPda1,
6147
+ riskNoncePdaForAlt,
6148
+ depositorAta
6149
+ ])
6150
+ );
6151
+ };
6152
+ const relayAltEligible = () => Boolean(relaySupplementalAlt && relayUrl && isDeposit && isSplPool && depositNonce && nullifiers);
6153
+ const getRelaySupplementalAlt = () => {
6154
+ if (!relaySupplementalAltPromise) {
6155
+ relaySupplementalAltPromise = requestRelaySupplementalAlt();
6156
+ }
6157
+ return relaySupplementalAltPromise;
6158
+ };
6159
+ const acquireDepositAlt = async () => {
6160
+ if (relayAltEligible()) {
6161
+ try {
6162
+ onProgress?.("Requesting lookup table from relay...");
6163
+ const relayAlt = await getRelaySupplementalAlt();
6164
+ return [relayAlt];
6165
+ } catch (err) {
6166
+ const msg = err instanceof Error ? err.message : String(err);
6167
+ onProgress?.(
6168
+ `Relay lookup table failed (${msg}); falling back to a depositor-signed table.`
6169
+ );
6170
+ }
6171
+ }
6172
+ onProgress?.("Creating address lookup table for V0 transaction...");
6173
+ return createEphemeralALT(connection, depositor, onProgress, [], externalFeePayer);
6174
+ };
5963
6175
  if ((isDeposit || isSend) && riskCheckEnabled && !riskQuoteIx) {
5964
6176
  if (getRiskQuoteInstruction) {
5965
6177
  onProgress?.("Fetching risk quote (custom)...");
@@ -5990,10 +6202,13 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
5990
6202
  }
5991
6203
  if (depositNonce) {
5992
6204
  [riskNoncePda] = deriveRiskNoncePDA(programId, depositNonce);
6205
+ if (relaySupplementalAlt && relayUrl && isSplPool) {
6206
+ getRelaySupplementalAlt().catch(() => {
6207
+ });
6208
+ }
5993
6209
  }
5994
- if (riskQuoteIx && (!addressLookupTableAccounts || addressLookupTableAccounts.length === 0)) {
5995
- onProgress?.("Creating address lookup table for V0 transaction (risk quote)...");
5996
- addressLookupTableAccounts = await createEphemeralALT(connection, depositor, onProgress, [], externalFeePayer);
6210
+ if ((!addressLookupTableAccounts || addressLookupTableAccounts.length === 0) && (riskQuoteIx || needsAltForChainNotes || needsAltForSplDeposit)) {
6211
+ addressLookupTableAccounts = await acquireDepositAlt();
5997
6212
  }
5998
6213
  }
5999
6214
  if (!isDeposit && riskCheckEnabled && recipient && riskQuoteUrl && !riskQuoteIx) {
@@ -6059,7 +6274,14 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
6059
6274
  externalFeePayerPubkey,
6060
6275
  fullInstructions,
6061
6276
  addressLookupTableAccounts,
6062
- onProgress
6277
+ onProgress,
6278
+ void 0,
6279
+ // `addressLookupTableAccounts` above already carries the relay-owned table when eligible
6280
+ // (acquireDepositAlt ran before this branch, same as the non-externalFeePayer path). This is
6281
+ // only for submitViaExternalFeePayer's OWN, separate supplemental round — triggered by the
6282
+ // external payer's extra signature and payment-instruction accounts pushing an already-ALT'd
6283
+ // tx back over the limit — so that round also tries the relay before an ephemeral fallback.
6284
+ relayAltEligible() ? getRelaySupplementalAlt : void 0
6063
6285
  );
6064
6286
  } else if (useV0) {
6065
6287
  const plan = await planDirectV0Submission({
@@ -6068,6 +6290,23 @@ async function submitTransactionDirect(connection, programId, depositor, proofBy
6068
6290
  compact: compactInstructions,
6069
6291
  minimal: minimalInstructions,
6070
6292
  createSupplementalAlt: async () => {
6293
+ if (relayAltEligible()) {
6294
+ try {
6295
+ onProgress?.("Requesting supplemental lookup table from relay...");
6296
+ const relayAlt = await getRelaySupplementalAlt();
6297
+ addressLookupTableAccounts = [
6298
+ ...(addressLookupTableAccounts ?? []).filter((t) => !t.key.equals(relayAlt.key)),
6299
+ relayAlt
6300
+ ];
6301
+ ({ blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash());
6302
+ return;
6303
+ } catch (err) {
6304
+ const msg = err instanceof Error ? err.message : String(err);
6305
+ onProgress?.(
6306
+ `Relay supplemental ALT failed (${msg}); falling back to a depositor-signed table.`
6307
+ );
6308
+ }
6309
+ }
6071
6310
  const supplementalAddresses = collectLookupCandidatesFromInstructions(
6072
6311
  minimalInstructions,
6073
6312
  depositor.publicKey,
@@ -6860,6 +7099,21 @@ async function transact(params, options) {
6860
7099
  "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."
6861
7100
  );
6862
7101
  }
7102
+ const chainNoteSalt = options.chainNoteSalt !== void 0 ? assertChainNoteSalt(options.chainNoteSalt) : randomChainNoteSalt();
7103
+ const chainNoteWillCarrySalt = !options.disableChainNotes && !(options.encryptedNotes && options.encryptedNotes.length > 0);
7104
+ if (explicitChainNoteNk && chainNoteWillCarrySalt) {
7105
+ const inputOwners = new Set(inputUtxos.map((u) => u.keypair.publicKey));
7106
+ for (let i = 0; i < outputUtxos.length; i++) {
7107
+ const out = outputUtxos[i];
7108
+ if (!out || out.amount <= BigInt(0)) continue;
7109
+ if (!inputOwners.has(out.keypair.publicKey)) continue;
7110
+ if (out.blinding !== deriveChangeNoteBlinding(explicitChainNoteNk, chainNoteSalt, i)) {
7111
+ throw new Error(
7112
+ `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.`
7113
+ );
7114
+ }
7115
+ }
7116
+ }
6863
7117
  assertAllowedRpcConnection(connection);
6864
7118
  await ensureViewingKeyRegistered(options, onProgress, fallbackNk);
6865
7119
  const resolvedChainNoteNk = explicitChainNoteNk ?? fallbackNk ?? null;
@@ -7214,7 +7468,6 @@ async function transact(params, options) {
7214
7468
  const maxFee = isWithdrawal ? protocolFeeSnapshot : BigInt(0);
7215
7469
  const extDataHash = await computeExtDataHash(recipient ?? null, relayerFee, relayer ?? null, maxFee);
7216
7470
  const chainNoteTimestamp = BigInt(Date.now());
7217
- const chainNoteSalt = options.chainNoteSalt !== void 0 ? assertChainNoteSalt(options.chainNoteSalt) : randomChainNoteSalt();
7218
7471
  const mintField = pubkeyToFieldElement(mint);
7219
7472
  let signature = "pending-" + Date.now().toString();
7220
7473
  let commitmentIndices = [-1, -1];
@@ -7489,7 +7742,8 @@ async function transact(params, options) {
7489
7742
  relayer ?? null,
7490
7743
  relayerFee,
7491
7744
  options.onTransactProofBuilt,
7492
- externalFeePayer
7745
+ externalFeePayer,
7746
+ options.relaySupplementalAlt
7493
7747
  );
7494
7748
  signature = directResult.signature;
7495
7749
  commitmentIndices = directResult.commitmentIndices;
@@ -7881,16 +8135,30 @@ async function transfer(inputUtxos, recipientPubkey, amount, options) {
7881
8135
  const change = inputSum - amount;
7882
8136
  const myKeypair = inputUtxos[0].keypair;
7883
8137
  const mint = inputUtxos[0].mintAddress;
8138
+ const transferNk = await resolveChainNoteNk(options);
8139
+ const changeNoteSalt = transferNk ? randomChangeNoteSalt() : void 0;
8140
+ const isSendToSelf = inputUtxos.some((u) => u.keypair.publicKey === recipientPubkey);
7884
8141
  const recipientKeypair = {
7885
8142
  privateKey: BigInt(0),
7886
8143
  // Recipient's private key unknown
7887
8144
  publicKey: recipientPubkey
7888
8145
  };
7889
- const recipientUtxo = await createUtxo(amount, recipientKeypair, mint);
8146
+ const recipientUtxo = transferNk && isSendToSelf ? (await createRecoverableChangeUtxo(amount, myKeypair, transferNk, mint, changeNoteSalt, 0)).utxo : await createUtxo(amount, recipientKeypair, mint);
7890
8147
  const outputUtxos = [recipientUtxo];
7891
8148
  if (change > BigInt(0)) {
7892
- const changeUtxo = await createUtxo(change, myKeypair, mint);
7893
- outputUtxos.push(changeUtxo);
8149
+ if (transferNk) {
8150
+ const { utxo } = await createRecoverableChangeUtxo(
8151
+ change,
8152
+ myKeypair,
8153
+ transferNk,
8154
+ mint,
8155
+ changeNoteSalt,
8156
+ 1
8157
+ );
8158
+ outputUtxos.push(utxo);
8159
+ } else {
8160
+ outputUtxos.push(await createUtxo(change, myKeypair, mint));
8161
+ }
7894
8162
  }
7895
8163
  return transact(
7896
8164
  {
@@ -7899,7 +8167,7 @@ async function transfer(inputUtxos, recipientPubkey, amount, options) {
7899
8167
  externalAmount: BigInt(0)
7900
8168
  // Pure shield-to-shield
7901
8169
  },
7902
- options
8170
+ changeNoteSalt !== void 0 ? { ...options, chainNoteSalt: changeNoteSalt } : options
7903
8171
  );
7904
8172
  }
7905
8173
  async function partialWithdraw(inputUtxos, recipient, withdrawAmount, options) {
@@ -7916,7 +8184,9 @@ async function partialWithdraw(inputUtxos, recipient, withdrawAmount, options) {
7916
8184
  utxos.sort((a, b) => a.amount > b.amount ? 1 : a.amount < b.amount ? -1 : 0);
7917
8185
  const [small1, small2, ...rest] = utxos;
7918
8186
  const mergedAmount = small1.amount + small2.amount;
7919
- const mergedOutput = await createUtxo(mergedAmount, myKeypair, mint);
8187
+ const mergeNk = await resolveChainNoteNk(options);
8188
+ const mergeSalt = mergeNk ? randomChangeNoteSalt() : void 0;
8189
+ const mergedOutput = mergeNk ? (await createRecoverableChangeUtxo(mergedAmount, myKeypair, mergeNk, mint, mergeSalt, 0)).utxo : await createUtxo(mergedAmount, myKeypair, mint);
7920
8190
  const mergeResult = await transact(
7921
8191
  {
7922
8192
  inputUtxos: [small1, small2],
@@ -7926,7 +8196,8 @@ async function partialWithdraw(inputUtxos, recipient, withdrawAmount, options) {
7926
8196
  },
7927
8197
  {
7928
8198
  ...options,
7929
- cachedMerkleTree: cachedTree
8199
+ cachedMerkleTree: cachedTree,
8200
+ ...mergeSalt !== void 0 && { chainNoteSalt: mergeSalt }
7930
8201
  }
7931
8202
  );
7932
8203
  cachedTree = mergeResult.merkleTree;
@@ -7939,9 +8210,25 @@ async function partialWithdraw(inputUtxos, recipient, withdrawAmount, options) {
7939
8210
  const finalInputSum = sumUtxoAmounts(utxos);
7940
8211
  const change = finalInputSum - withdrawAmount;
7941
8212
  const outputUtxos = [];
8213
+ let changeNoteSalt;
7942
8214
  if (change > BigInt(0)) {
7943
- const changeUtxo = await createUtxo(change, myKeypair, mint);
7944
- outputUtxos.push(changeUtxo);
8215
+ const changeNk = await resolveChainNoteNk(options);
8216
+ if (changeNk) {
8217
+ const salt = randomChangeNoteSalt();
8218
+ const { utxo } = await createRecoverableChangeUtxo(
8219
+ change,
8220
+ myKeypair,
8221
+ changeNk,
8222
+ mint,
8223
+ salt,
8224
+ 0
8225
+ // partialWithdraw puts change at output 0
8226
+ );
8227
+ outputUtxos.push(utxo);
8228
+ changeNoteSalt = salt;
8229
+ } else {
8230
+ outputUtxos.push(await createUtxo(change, myKeypair, mint));
8231
+ }
7945
8232
  }
7946
8233
  return transact(
7947
8234
  {
@@ -7952,7 +8239,8 @@ async function partialWithdraw(inputUtxos, recipient, withdrawAmount, options) {
7952
8239
  },
7953
8240
  {
7954
8241
  ...options,
7955
- cachedMerkleTree: cachedTree
8242
+ cachedMerkleTree: cachedTree,
8243
+ ...changeNoteSalt !== void 0 && { chainNoteSalt: changeNoteSalt }
7956
8244
  }
7957
8245
  );
7958
8246
  }
@@ -8086,7 +8374,16 @@ async function swapUtxo(params, options) {
8086
8374
  onProgress?.("Validating transaction parameters...");
8087
8375
  const swapFallbackNk = getNkFromUtxoPrivateKey(inputUtxos[0].keypair.privateKey);
8088
8376
  await ensureViewingKeyRegistered(options, onProgress, swapFallbackNk);
8089
- const resolvedSwapChainNoteNk = await resolveChainNoteNk(options) ?? swapFallbackNk;
8377
+ const explicitSwapChainNoteNk = await resolveChainNoteNk(options);
8378
+ const resolvedSwapChainNoteNk = explicitSwapChainNoteNk ?? swapFallbackNk;
8379
+ const chainNoteSalt = options.chainNoteSalt !== void 0 ? assertChainNoteSalt(options.chainNoteSalt) : randomChainNoteSalt();
8380
+ if (explicitSwapChainNoteNk && changeUtxo && changeUtxo.amount > BigInt(0) && inputUtxos.some((u) => u.keypair.publicKey === changeUtxo.keypair.publicKey)) {
8381
+ if (changeUtxo.blinding !== deriveChangeNoteBlinding(explicitSwapChainNoteNk, chainNoteSalt, 0)) {
8382
+ throw new Error(
8383
+ `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.`
8384
+ );
8385
+ }
8386
+ }
8090
8387
  const swapPoolMint = NATIVE_SOL_MINT;
8091
8388
  const pdas = getShieldPoolPDAs(programId, swapPoolMint);
8092
8389
  let merkleState = null;
@@ -8428,7 +8725,6 @@ async function swapUtxo(params, options) {
8428
8725
  outputCommitments.push(await computeCommitment(utxo));
8429
8726
  }
8430
8727
  const chainNoteTimestamp = BigInt(Date.now());
8431
- const chainNoteSalt = randomChainNoteSalt();
8432
8728
  onProgress?.("Computing external data hash...");
8433
8729
  const mint = swapPoolMint;
8434
8730
  const mintField = pubkeyToFieldElement(mint);
@@ -8646,6 +8942,9 @@ async function swapUtxo(params, options) {
8646
8942
  url.searchParams.set("wallet", poolPda.toBase58());
8647
8943
  url.searchParams.set("recipient", recipientWallet.toBase58());
8648
8944
  url.searchParams.set("sender", recipientWallet.toBase58());
8945
+ url.searchParams.set("pool_mint", mint.toBase58());
8946
+ url.searchParams.set("nullifier0", toHex(bigintToBytes322(inputNullifiers[0])));
8947
+ url.searchParams.set("nullifier1", toHex(bigintToBytes322(inputNullifiers[1])));
8649
8948
  const quoteRes = await relayFetch(url.toString());
8650
8949
  if (!quoteRes.ok) {
8651
8950
  const text = await quoteRes.text();
@@ -8890,10 +9189,18 @@ async function swapWithChange(inputUtxos, swapAmount, outputMint, recipientAta,
8890
9189
  throw new Error(`Insufficient balance: input(${totalInput}) < swap(${swapAmount})`);
8891
9190
  }
8892
9191
  let changeUtxo;
9192
+ let changeNoteSalt;
8893
9193
  if (change > BigInt(0)) {
8894
9194
  const myKeypair = inputUtxos[0].keypair;
8895
9195
  const mint = inputUtxos[0].mintAddress;
8896
- changeUtxo = await createUtxo(change, myKeypair, mint);
9196
+ const changeNk = await resolveChainNoteNk(options);
9197
+ if (changeNk) {
9198
+ const salt = randomChangeNoteSalt();
9199
+ changeUtxo = (await createRecoverableChangeUtxo(change, myKeypair, changeNk, mint, salt, 0)).utxo;
9200
+ changeNoteSalt = salt;
9201
+ } else {
9202
+ changeUtxo = await createUtxo(change, myKeypair, mint);
9203
+ }
8897
9204
  }
8898
9205
  return swapUtxo(
8899
9206
  {
@@ -8905,7 +9212,7 @@ async function swapWithChange(inputUtxos, swapAmount, outputMint, recipientAta,
8905
9212
  recipientWallet,
8906
9213
  minOutputAmount
8907
9214
  },
8908
- options
9215
+ changeNoteSalt !== void 0 ? { ...options, chainNoteSalt: changeNoteSalt } : options
8909
9216
  );
8910
9217
  }
8911
9218
 
@@ -9708,6 +10015,7 @@ async function scanTransactions(opts) {
9708
10015
  const txs = [];
9709
10016
  const swapCtxByCommitment = /* @__PURE__ */ new Map();
9710
10017
  const recoveredDepositNotes = [];
10018
+ const recoveredChangeNotes = [];
9711
10019
  let processed = 0;
9712
10020
  let rpcCallsMade = 0;
9713
10021
  onStatus?.(`Scanning ${sigInfos.length} transactions...`);
@@ -10055,6 +10363,34 @@ async function scanTransactions(opts) {
10055
10363
  if (debug) console.log(`[DEBUG] ${sigInfo.signature}: deposit recovery failed (${e?.message || e})`);
10056
10364
  }
10057
10365
  }
10366
+ if (isWithdrawal && compactNote.noteSalt !== void 0 && compactNote.outAmount0 !== void 0 && compactNote.outAmount0 > 0n && compactNote.outPubkey0 !== void 0 && compactNote.outPubkey0 !== 0n) {
10367
+ try {
10368
+ const recoveredChange = await matchChangeNote({
10369
+ viewingKeyNk,
10370
+ noteSalt: compactNote.noteSalt,
10371
+ amount: compactNote.outAmount0,
10372
+ keypair: { privateKey: 0n, publicKey: compactNote.outPubkey0 },
10373
+ mintAddress: new PublicKey9(asset.mint),
10374
+ outputIndex: 0,
10375
+ // v4 describes output 0, which is where change lands
10376
+ outputCommitments: ixCtx.outputCommitments ?? []
10377
+ });
10378
+ if (recoveredChange) {
10379
+ recoveredChangeNotes.push({
10380
+ ...recoveredChange,
10381
+ signature: sigInfo.signature,
10382
+ timestamp: decoded.timestamp
10383
+ });
10384
+ if (debug) {
10385
+ console.log(
10386
+ `[DEBUG] ${sigInfo.signature}: recovered change note (amount=${recoveredChange.amount})`
10387
+ );
10388
+ }
10389
+ }
10390
+ } catch (e) {
10391
+ if (debug) console.log(`[DEBUG] ${sigInfo.signature}: change recovery failed (${e?.message || e})`);
10392
+ }
10393
+ }
10058
10394
  txs.push({
10059
10395
  txType: decoded.txType,
10060
10396
  amount: grossAmount,
@@ -10160,7 +10496,8 @@ async function scanTransactions(opts) {
10160
10496
  lastSignature: newestSignature,
10161
10497
  rpcCallsMade,
10162
10498
  deliveredNotes,
10163
- recoveredDepositNotes
10499
+ recoveredDepositNotes,
10500
+ recoveredChangeNotes
10164
10501
  };
10165
10502
  }
10166
10503
  function toComplianceReport(result) {
@@ -10606,12 +10943,14 @@ export {
10606
10943
  createCloakError,
10607
10944
  createDepositInstruction,
10608
10945
  createLogger,
10946
+ createRecoverableChangeUtxo,
10609
10947
  createRecoverableDepositUtxo,
10610
10948
  createUtxo,
10611
10949
  createZeroUtxo,
10612
10950
  decryptCompactChainNote,
10613
10951
  decryptComplianceMetadataWithMasterKey,
10614
10952
  decryptTransactionMetadata,
10953
+ deriveChangeNoteBlinding,
10615
10954
  deriveDepositNoteSecrets,
10616
10955
  deriveDiversifiedViewingKey,
10617
10956
  deriveDiversifier,
@@ -10697,6 +11036,7 @@ export {
10697
11036
  loadPendingDeposits,
10698
11037
  loadPendingWithdrawals,
10699
11038
  loadVerifiedCircuitArtifacts,
11039
+ matchChangeNote,
10700
11040
  matchDepositNote,
10701
11041
  matchSwapRefundLeaf,
10702
11042
  openRecipientDeliveryNote,
@@ -10717,6 +11057,7 @@ export {
10717
11057
  pubkeyToFieldElement,
10718
11058
  pubkeyToLimbs,
10719
11059
  randomBytes,
11060
+ randomChangeNoteSalt,
10720
11061
  randomDepositNoteSalt,
10721
11062
  randomFieldElement,
10722
11063
  readMerkleTreeState,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cloak.dev/sdk",
3
3
  "description": "Shield, send, and swap on Solana privately — TypeScript SDK with UTXO-based zero-knowledge transactions",
4
- "version": "0.2.1",
4
+ "version": "0.2.2-staging.33f11a2",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.js",