@cloak.dev/sdk 0.1.4 → 0.1.5

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
@@ -301,12 +301,15 @@ __export(index_exports, {
301
301
  MemoryStorageAdapter: () => MemoryStorageAdapter,
302
302
  MerkleTree: () => MerkleTree,
303
303
  NATIVE_SOL_MINT: () => NATIVE_SOL_MINT,
304
+ RelayInternalError: () => RelayInternalError,
304
305
  RelayService: () => RelayService,
305
306
  RootNotFoundError: () => RootNotFoundError,
306
307
  SCANNER_SUPPORTS_TRANSACT_SWAP: () => SCANNER_SUPPORTS_TRANSACT_SWAP,
307
308
  SIGN_IN_MESSAGE: () => SIGN_IN_MESSAGE,
309
+ SanctionsQuoteError: () => SanctionsQuoteError,
308
310
  ShieldPoolErrors: () => ShieldPoolErrors,
309
311
  SimpleWallet: () => SimpleWallet,
312
+ UtxoAlreadySpentError: () => UtxoAlreadySpentError,
310
313
  UtxoWallet: () => UtxoWallet,
311
314
  VARIABLE_FEE_DENOMINATOR: () => VARIABLE_FEE_DENOMINATOR,
312
315
  VARIABLE_FEE_NUMERATOR: () => VARIABLE_FEE_NUMERATOR,
@@ -325,6 +328,7 @@ __export(index_exports, {
325
328
  calculateRelayFee: () => calculateRelayFee,
326
329
  chainNoteFromBase64: () => chainNoteFromBase64,
327
330
  chainNoteToBase64: () => chainNoteToBase64,
331
+ classifyRelayError: () => classifyRelayError,
328
332
  cleanupStalePendingOperations: () => cleanupStalePendingOperations,
329
333
  clearPendingDeposits: () => clearPendingDeposits,
330
334
  clearPendingWithdrawals: () => clearPendingWithdrawals,
@@ -437,6 +441,7 @@ __export(index_exports, {
437
441
  partialWithdraw: () => partialWithdraw,
438
442
  poseidonHash: () => poseidonHash,
439
443
  preflightCheck: () => preflightCheck,
444
+ preflightNullifiers: () => preflightNullifiers,
440
445
  prepareEncryptedOutput: () => prepareEncryptedOutput,
441
446
  prepareEncryptedOutputForRecipient: () => prepareEncryptedOutputForRecipient,
442
447
  proofToBytes: () => proofToBytes,
@@ -484,6 +489,7 @@ __export(index_exports, {
484
489
  validateWithdrawableNote: () => validateWithdrawableNote,
485
490
  verifyAllCircuits: () => verifyAllCircuits,
486
491
  verifyCircuitIntegrity: () => verifyCircuitIntegrity,
492
+ verifyUtxos: () => verifyUtxos,
487
493
  waitForRoot: () => waitForRoot,
488
494
  withTiming: () => withTiming
489
495
  });
@@ -1339,6 +1345,71 @@ function parseRelayErrorResponse(responseText) {
1339
1345
  return { isRootNotFound: false };
1340
1346
  }
1341
1347
  }
1348
+ var UtxoAlreadySpentError = class extends Error {
1349
+ constructor(message, spentUtxoCommitments = []) {
1350
+ super(message);
1351
+ this.spentUtxoCommitments = spentUtxoCommitments;
1352
+ this.name = "UtxoAlreadySpentError";
1353
+ }
1354
+ };
1355
+ var SanctionsQuoteError = class extends Error {
1356
+ constructor(message, onChainCode, subKind) {
1357
+ super(message);
1358
+ this.onChainCode = onChainCode;
1359
+ this.subKind = subKind;
1360
+ this.name = "SanctionsQuoteError";
1361
+ }
1362
+ };
1363
+ var RelayInternalError = class extends Error {
1364
+ constructor(message, relayMessage, httpStatus, cachedTreeFromChain) {
1365
+ super(message);
1366
+ this.relayMessage = relayMessage;
1367
+ this.httpStatus = httpStatus;
1368
+ this.cachedTreeFromChain = cachedTreeFromChain;
1369
+ this.name = "RelayInternalError";
1370
+ }
1371
+ };
1372
+ function classifyRelayError(responseText, httpStatus) {
1373
+ let inner = "";
1374
+ try {
1375
+ const parsed = JSON.parse(responseText);
1376
+ inner = parsed?.message ?? "";
1377
+ } catch {
1378
+ inner = responseText;
1379
+ }
1380
+ const haystack = inner.toLowerCase();
1381
+ if (haystack.includes("0x1020") || haystack.includes("doublespend")) {
1382
+ return new UtxoAlreadySpentError(
1383
+ "This shielded balance was already spent. The UTXO's nullifier is registered on-chain \u2014 typically means you spent it via another session or device. Run verifyUtxos() to reconcile local state."
1384
+ );
1385
+ }
1386
+ if (haystack.includes("0x10b0") || haystack.includes("rangequoteexpired")) {
1387
+ return new SanctionsQuoteError(
1388
+ "Range sanctions quote expired before the on-chain program processed the transaction. The relay's quote-expiry window is too short relative to proof-gen + confirmation latency, or on-chain clock drifted. Retry the send; the relay will issue a fresh quote.",
1389
+ 4272,
1390
+ "expired"
1391
+ );
1392
+ }
1393
+ if (haystack.includes("0x10b2") || haystack.includes("rangequotewalletmismatch")) {
1394
+ return new SanctionsQuoteError(
1395
+ "Range sanctions quote's wallet doesn't match the on-chain expected wallet. Indicates a relay config drift \u2014 the sender used for quote signing doesn't match the program's pool.range_signer / fee payer. Relay operators must fix; caller retry will not help.",
1396
+ 4274,
1397
+ "wallet_mismatch"
1398
+ );
1399
+ }
1400
+ if (haystack.includes("0x10b3") || haystack.includes("rangequotemissinged25519")) {
1401
+ return new SanctionsQuoteError(
1402
+ "Transaction reached the on-chain program without the required Ed25519 sanctions quote instruction. This is a relay bug \u2014 the relay constructed the tx without attaching the signed quote. Caller retry will not help.",
1403
+ 4275,
1404
+ "missing_ix"
1405
+ );
1406
+ }
1407
+ return new RelayInternalError(
1408
+ `Relay returned ${httpStatus ?? "an error"}: ${inner || responseText}`,
1409
+ inner || responseText,
1410
+ httpStatus
1411
+ );
1412
+ }
1342
1413
  var ShieldPoolErrors = {
1343
1414
  // Root management errors
1344
1415
  4096: "Invalid Merkle root",
@@ -4635,6 +4706,64 @@ async function decryptComplianceMetadataWithMasterKey(encrypted, masterComplianc
4635
4706
  return decryptWithSharedSecret(payload, sharedSecret);
4636
4707
  }
4637
4708
 
4709
+ // src/utils/verify-utxos.ts
4710
+ init_utxo();
4711
+ async function verifyUtxos(utxos, connection, programId, commitment = "confirmed") {
4712
+ const checkable = [];
4713
+ const skipped = [];
4714
+ for (const utxo of utxos) {
4715
+ if (utxo.commitment === void 0 || utxo.mintAddress === void 0 || utxo.keypair === void 0 || utxo.keypair.privateKey === void 0) {
4716
+ skipped.push(utxo);
4717
+ continue;
4718
+ }
4719
+ try {
4720
+ const nullifierBig = await computeNullifier2(utxo);
4721
+ const nullifierHex = nullifierBig.toString(16).padStart(64, "0");
4722
+ const nullifierBytes = Buffer.from(nullifierHex, "hex");
4723
+ const { pool } = getShieldPoolPDAs(programId, utxo.mintAddress);
4724
+ const [pda] = getNullifierPDA(pool, nullifierBytes, programId);
4725
+ checkable.push({ utxo, pda });
4726
+ } catch {
4727
+ skipped.push(utxo);
4728
+ }
4729
+ }
4730
+ if (checkable.length === 0) {
4731
+ return { spent: [], unspent: [], skipped };
4732
+ }
4733
+ const CHUNK = 100;
4734
+ const infos = [];
4735
+ for (let i = 0; i < checkable.length; i += CHUNK) {
4736
+ const page = checkable.slice(i, i + CHUNK).map((e) => e.pda);
4737
+ const pageInfos = await connection.getMultipleAccountsInfo(page, commitment);
4738
+ infos.push(...pageInfos);
4739
+ }
4740
+ const spent = [];
4741
+ const unspent = [];
4742
+ for (let i = 0; i < checkable.length; i++) {
4743
+ if (infos[i] !== null) {
4744
+ spent.push(checkable[i].utxo);
4745
+ } else {
4746
+ unspent.push(checkable[i].utxo);
4747
+ }
4748
+ }
4749
+ return { spent, unspent, skipped };
4750
+ }
4751
+ async function preflightNullifiers(utxos, connection, programId, commitment = "confirmed") {
4752
+ if (utxos.length === 0) return;
4753
+ const result = await verifyUtxos(utxos, connection, programId, commitment);
4754
+ if (result.spent.length > 0) {
4755
+ const allCommitments = result.spent.map(
4756
+ (u) => u.commitment !== void 0 ? u.commitment.toString(16).padStart(64, "0") : "<unknown>"
4757
+ );
4758
+ const preview = allCommitments.slice(0, 5);
4759
+ const extra = allCommitments.length > preview.length ? ` (+${allCommitments.length - preview.length} more)` : "";
4760
+ throw new UtxoAlreadySpentError(
4761
+ `Pre-flight detected ${allCommitments.length} already-spent UTXO(s). First spent commitment(s): ${preview.join(", ")}${extra}. Call verifyUtxos() and remove these from local state before retrying.`,
4762
+ allCommitments
4763
+ );
4764
+ }
4765
+ }
4766
+
4638
4767
  // src/services/risk-oracle.ts
4639
4768
  function buildRangeRiskScoreJob(OracleJob, walletAddress) {
4640
4769
  return OracleJob.fromObject({
@@ -4981,10 +5110,15 @@ var import_bs58 = __toESM(require("bs58"), 1);
4981
5110
  var bs58 = import_bs58.default.default || import_bs58.default;
4982
5111
  var TRANSACT_TAG = 0;
4983
5112
  var TRANSACT_SWAP_TAG = 1;
4984
- var DEPOSIT_TAG = 1;
4985
5113
  var PROOF_LEN = 256;
4986
5114
  var PUBLIC_INPUTS_LEN = 264;
4987
5115
  var COMMITMENTS_OFFSET = 168;
5116
+ function isAllZero(b) {
5117
+ for (let i = 0; i < b.length; i++) {
5118
+ if (b[i] !== 0) return false;
5119
+ }
5120
+ return true;
5121
+ }
4988
5122
  var DEFAULT_TIMEOUT_MS = 3e4;
4989
5123
  var DEFAULT_MAX_RETRIES = 3;
4990
5124
  async function fetchWithRetry(url, options = {}) {
@@ -5122,24 +5256,20 @@ async function buildMerkleTreeFromRelay(relayUrl, options) {
5122
5256
  function extractCommitmentsFromInstruction(data) {
5123
5257
  if (data.length === 0) return [];
5124
5258
  const tag = data[0];
5125
- if (tag === TRANSACT_TAG || tag === TRANSACT_SWAP_TAG) {
5126
- const publicInputsStart = 1 + PROOF_LEN;
5127
- const publicInputsEnd = publicInputsStart + PUBLIC_INPUTS_LEN;
5128
- if (data.length < publicInputsEnd) return [];
5129
- const publicInputs = data.slice(publicInputsStart, publicInputsEnd);
5130
- const commitments = [];
5131
- const c0 = publicInputs.slice(COMMITMENTS_OFFSET, COMMITMENTS_OFFSET + 32);
5132
- const c1 = publicInputs.slice(COMMITMENTS_OFFSET + 32, COMMITMENTS_OFFSET + 64);
5133
- commitments.push(c0);
5134
- commitments.push(c1);
5135
- return commitments;
5136
- }
5137
- if (tag === DEPOSIT_TAG) {
5138
- if (data.length < 1 + 8 + 32) return [];
5139
- const commitment = data.slice(1 + 8, 1 + 8 + 32);
5140
- return [commitment];
5141
- }
5142
- return [];
5259
+ if (tag !== TRANSACT_TAG && tag !== TRANSACT_SWAP_TAG) return [];
5260
+ const publicInputsStart = 1 + PROOF_LEN;
5261
+ const publicInputsEnd = publicInputsStart + PUBLIC_INPUTS_LEN;
5262
+ if (data.length < publicInputsEnd) return [];
5263
+ const publicInputs = data.slice(publicInputsStart, publicInputsEnd);
5264
+ const c0 = publicInputs.slice(COMMITMENTS_OFFSET, COMMITMENTS_OFFSET + 32);
5265
+ const c1 = publicInputs.slice(
5266
+ COMMITMENTS_OFFSET + 32,
5267
+ COMMITMENTS_OFFSET + 64
5268
+ );
5269
+ const out = [];
5270
+ if (!isAllZero(c0)) out.push(c0);
5271
+ if (!isAllZero(c1)) out.push(c1);
5272
+ return out;
5143
5273
  }
5144
5274
  function isBrowser() {
5145
5275
  return typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
@@ -6746,6 +6876,7 @@ async function transact(params, options) {
6746
6876
  if (outputUtxos.length > 2) {
6747
6877
  throw new Error("Maximum 2 output UTXOs allowed");
6748
6878
  }
6879
+ await preflightNullifiers(inputUtxos, connection, programId);
6749
6880
  const mint = inputUtxos.find((u) => u.amount > 0)?.mintAddress ?? outputUtxos.find((u) => u.amount > 0)?.mintAddress ?? NATIVE_SOL_MINT;
6750
6881
  const paddedInputs = [...inputUtxos];
6751
6882
  while (paddedInputs.length < 2) {
@@ -7573,10 +7704,46 @@ async function transact(params, options) {
7573
7704
  }
7574
7705
  }
7575
7706
  if (lastError) {
7707
+ const classified = classifyRelayError(lastError.message);
7708
+ const isMerkleClass = !(classified instanceof UtxoAlreadySpentError) && !(classified instanceof SanctionsQuoteError) && !isRootNotFoundError2(lastError);
7709
+ if (!IS_BROWSER2 && isMerkleClass && relayUrl) {
7710
+ onProgress?.(
7711
+ "All relay-tree attempts failed. Rebuilding merkle tree from chain as last resort..."
7712
+ );
7713
+ try {
7714
+ const [merkleTreePda] = import_web37.PublicKey.findProgramAddressSync(
7715
+ [Buffer.from("merkle_tree"), mint.toBuffer()],
7716
+ programId
7717
+ );
7718
+ const chainTree = await buildMerkleTreeFromChain(
7719
+ connection,
7720
+ programId,
7721
+ merkleTreePda,
7722
+ onProgress
7723
+ );
7724
+ const chainRootHex = chainTree.root().toString(16).padStart(64, "0");
7725
+ onProgress?.(
7726
+ `Chain-rebuilt tree root: ${chainRootHex.substring(0, 16)}... \u2014 attached to error for caller retry`
7727
+ );
7728
+ throw new RelayInternalError(
7729
+ `All relay-tree attempts failed. Chain-rebuilt tree available (root ${chainRootHex.substring(0, 16)}...). Retry this call with options.cachedMerkleTree = err.cachedTreeFromChain. Original error: ${lastError.message}`,
7730
+ lastError.message,
7731
+ void 0,
7732
+ chainTree
7733
+ );
7734
+ } catch (e) {
7735
+ if (e instanceof RelayInternalError) throw e;
7736
+ onProgress?.(
7737
+ `Chain-replay fallback failed: ${e instanceof Error ? e.message : String(e)}`
7738
+ );
7739
+ }
7740
+ }
7576
7741
  if (isRootNotFoundError2(lastError)) {
7577
- throw new Error(`Transaction failed after ${maxRootRetries + 1} retries: ${lastError.message}`);
7742
+ throw new Error(
7743
+ `Transaction failed after ${maxRootRetries + 1} retries: ${lastError.message}`
7744
+ );
7578
7745
  }
7579
- throw new Error(`Transaction failed: ${lastError.message}`);
7746
+ throw classified;
7580
7747
  }
7581
7748
  if (relayUrl && outputCommitments.length >= 2) {
7582
7749
  const hasOnChainIndices = commitmentIndices[0] >= 0 && commitmentIndices[1] >= 0;
@@ -7880,6 +8047,7 @@ async function swapUtxo(params, options) {
7880
8047
  if (inputUtxos.length > 2) {
7881
8048
  throw new Error("Maximum 2 input UTXOs allowed");
7882
8049
  }
8050
+ await preflightNullifiers(inputUtxos, connection, programId);
7883
8051
  const outputMintAccount = await connection.getAccountInfo(outputMint, {
7884
8052
  commitment: "confirmed"
7885
8053
  });
@@ -9703,7 +9871,7 @@ var SimpleWallet = class {
9703
9871
  };
9704
9872
 
9705
9873
  // src/index.ts
9706
- var VERSION = "1.0.0";
9874
+ var VERSION = "0.1.5";
9707
9875
  var SCANNER_SUPPORTS_TRANSACT_SWAP = true;
9708
9876
  // Annotate the CommonJS export names for ESM import in node:
9709
9877
  0 && (module.exports = {
@@ -9721,12 +9889,15 @@ var SCANNER_SUPPORTS_TRANSACT_SWAP = true;
9721
9889
  MemoryStorageAdapter,
9722
9890
  MerkleTree,
9723
9891
  NATIVE_SOL_MINT,
9892
+ RelayInternalError,
9724
9893
  RelayService,
9725
9894
  RootNotFoundError,
9726
9895
  SCANNER_SUPPORTS_TRANSACT_SWAP,
9727
9896
  SIGN_IN_MESSAGE,
9897
+ SanctionsQuoteError,
9728
9898
  ShieldPoolErrors,
9729
9899
  SimpleWallet,
9900
+ UtxoAlreadySpentError,
9730
9901
  UtxoWallet,
9731
9902
  VARIABLE_FEE_DENOMINATOR,
9732
9903
  VARIABLE_FEE_NUMERATOR,
@@ -9745,6 +9916,7 @@ var SCANNER_SUPPORTS_TRANSACT_SWAP = true;
9745
9916
  calculateRelayFee,
9746
9917
  chainNoteFromBase64,
9747
9918
  chainNoteToBase64,
9919
+ classifyRelayError,
9748
9920
  cleanupStalePendingOperations,
9749
9921
  clearPendingDeposits,
9750
9922
  clearPendingWithdrawals,
@@ -9857,6 +10029,7 @@ var SCANNER_SUPPORTS_TRANSACT_SWAP = true;
9857
10029
  partialWithdraw,
9858
10030
  poseidonHash,
9859
10031
  preflightCheck,
10032
+ preflightNullifiers,
9860
10033
  prepareEncryptedOutput,
9861
10034
  prepareEncryptedOutputForRecipient,
9862
10035
  proofToBytes,
@@ -9904,6 +10077,7 @@ var SCANNER_SUPPORTS_TRANSACT_SWAP = true;
9904
10077
  validateWithdrawableNote,
9905
10078
  verifyAllCircuits,
9906
10079
  verifyCircuitIntegrity,
10080
+ verifyUtxos,
9907
10081
  waitForRoot,
9908
10082
  withTiming
9909
10083
  });