@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.js CHANGED
@@ -877,6 +877,71 @@ function parseRelayErrorResponse(responseText) {
877
877
  return { isRootNotFound: false };
878
878
  }
879
879
  }
880
+ var UtxoAlreadySpentError = class extends Error {
881
+ constructor(message, spentUtxoCommitments = []) {
882
+ super(message);
883
+ this.spentUtxoCommitments = spentUtxoCommitments;
884
+ this.name = "UtxoAlreadySpentError";
885
+ }
886
+ };
887
+ var SanctionsQuoteError = class extends Error {
888
+ constructor(message, onChainCode, subKind) {
889
+ super(message);
890
+ this.onChainCode = onChainCode;
891
+ this.subKind = subKind;
892
+ this.name = "SanctionsQuoteError";
893
+ }
894
+ };
895
+ var RelayInternalError = class extends Error {
896
+ constructor(message, relayMessage, httpStatus, cachedTreeFromChain) {
897
+ super(message);
898
+ this.relayMessage = relayMessage;
899
+ this.httpStatus = httpStatus;
900
+ this.cachedTreeFromChain = cachedTreeFromChain;
901
+ this.name = "RelayInternalError";
902
+ }
903
+ };
904
+ function classifyRelayError(responseText, httpStatus) {
905
+ let inner = "";
906
+ try {
907
+ const parsed = JSON.parse(responseText);
908
+ inner = parsed?.message ?? "";
909
+ } catch {
910
+ inner = responseText;
911
+ }
912
+ const haystack = inner.toLowerCase();
913
+ if (haystack.includes("0x1020") || haystack.includes("doublespend")) {
914
+ return new UtxoAlreadySpentError(
915
+ "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."
916
+ );
917
+ }
918
+ if (haystack.includes("0x10b0") || haystack.includes("rangequoteexpired")) {
919
+ return new SanctionsQuoteError(
920
+ "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.",
921
+ 4272,
922
+ "expired"
923
+ );
924
+ }
925
+ if (haystack.includes("0x10b2") || haystack.includes("rangequotewalletmismatch")) {
926
+ return new SanctionsQuoteError(
927
+ "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.",
928
+ 4274,
929
+ "wallet_mismatch"
930
+ );
931
+ }
932
+ if (haystack.includes("0x10b3") || haystack.includes("rangequotemissinged25519")) {
933
+ return new SanctionsQuoteError(
934
+ "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.",
935
+ 4275,
936
+ "missing_ix"
937
+ );
938
+ }
939
+ return new RelayInternalError(
940
+ `Relay returned ${httpStatus ?? "an error"}: ${inner || responseText}`,
941
+ inner || responseText,
942
+ httpStatus
943
+ );
944
+ }
880
945
  var ShieldPoolErrors = {
881
946
  // Root management errors
882
947
  4096: "Invalid Merkle root",
@@ -4173,6 +4238,63 @@ async function decryptComplianceMetadataWithMasterKey(encrypted, masterComplianc
4173
4238
  return decryptWithSharedSecret(payload, sharedSecret);
4174
4239
  }
4175
4240
 
4241
+ // src/utils/verify-utxos.ts
4242
+ async function verifyUtxos(utxos, connection, programId, commitment = "confirmed") {
4243
+ const checkable = [];
4244
+ const skipped = [];
4245
+ for (const utxo of utxos) {
4246
+ if (utxo.commitment === void 0 || utxo.mintAddress === void 0 || utxo.keypair === void 0 || utxo.keypair.privateKey === void 0) {
4247
+ skipped.push(utxo);
4248
+ continue;
4249
+ }
4250
+ try {
4251
+ const nullifierBig = await computeNullifier(utxo);
4252
+ const nullifierHex = nullifierBig.toString(16).padStart(64, "0");
4253
+ const nullifierBytes = Buffer.from(nullifierHex, "hex");
4254
+ const { pool } = getShieldPoolPDAs(programId, utxo.mintAddress);
4255
+ const [pda] = getNullifierPDA(pool, nullifierBytes, programId);
4256
+ checkable.push({ utxo, pda });
4257
+ } catch {
4258
+ skipped.push(utxo);
4259
+ }
4260
+ }
4261
+ if (checkable.length === 0) {
4262
+ return { spent: [], unspent: [], skipped };
4263
+ }
4264
+ const CHUNK = 100;
4265
+ const infos = [];
4266
+ for (let i = 0; i < checkable.length; i += CHUNK) {
4267
+ const page = checkable.slice(i, i + CHUNK).map((e) => e.pda);
4268
+ const pageInfos = await connection.getMultipleAccountsInfo(page, commitment);
4269
+ infos.push(...pageInfos);
4270
+ }
4271
+ const spent = [];
4272
+ const unspent = [];
4273
+ for (let i = 0; i < checkable.length; i++) {
4274
+ if (infos[i] !== null) {
4275
+ spent.push(checkable[i].utxo);
4276
+ } else {
4277
+ unspent.push(checkable[i].utxo);
4278
+ }
4279
+ }
4280
+ return { spent, unspent, skipped };
4281
+ }
4282
+ async function preflightNullifiers(utxos, connection, programId, commitment = "confirmed") {
4283
+ if (utxos.length === 0) return;
4284
+ const result = await verifyUtxos(utxos, connection, programId, commitment);
4285
+ if (result.spent.length > 0) {
4286
+ const allCommitments = result.spent.map(
4287
+ (u) => u.commitment !== void 0 ? u.commitment.toString(16).padStart(64, "0") : "<unknown>"
4288
+ );
4289
+ const preview = allCommitments.slice(0, 5);
4290
+ const extra = allCommitments.length > preview.length ? ` (+${allCommitments.length - preview.length} more)` : "";
4291
+ throw new UtxoAlreadySpentError(
4292
+ `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.`,
4293
+ allCommitments
4294
+ );
4295
+ }
4296
+ }
4297
+
4176
4298
  // src/services/risk-oracle.ts
4177
4299
  function buildRangeRiskScoreJob(OracleJob, walletAddress) {
4178
4300
  return OracleJob.fromObject({
@@ -4521,10 +4643,15 @@ import _bs58 from "bs58";
4521
4643
  var bs58 = _bs58.default || _bs58;
4522
4644
  var TRANSACT_TAG = 0;
4523
4645
  var TRANSACT_SWAP_TAG = 1;
4524
- var DEPOSIT_TAG = 1;
4525
4646
  var PROOF_LEN = 256;
4526
4647
  var PUBLIC_INPUTS_LEN = 264;
4527
4648
  var COMMITMENTS_OFFSET = 168;
4649
+ function isAllZero(b) {
4650
+ for (let i = 0; i < b.length; i++) {
4651
+ if (b[i] !== 0) return false;
4652
+ }
4653
+ return true;
4654
+ }
4528
4655
  var DEFAULT_TIMEOUT_MS = 3e4;
4529
4656
  var DEFAULT_MAX_RETRIES = 3;
4530
4657
  async function fetchWithRetry(url, options = {}) {
@@ -4662,24 +4789,20 @@ async function buildMerkleTreeFromRelay(relayUrl, options) {
4662
4789
  function extractCommitmentsFromInstruction(data) {
4663
4790
  if (data.length === 0) return [];
4664
4791
  const tag = data[0];
4665
- if (tag === TRANSACT_TAG || tag === TRANSACT_SWAP_TAG) {
4666
- const publicInputsStart = 1 + PROOF_LEN;
4667
- const publicInputsEnd = publicInputsStart + PUBLIC_INPUTS_LEN;
4668
- if (data.length < publicInputsEnd) return [];
4669
- const publicInputs = data.slice(publicInputsStart, publicInputsEnd);
4670
- const commitments = [];
4671
- const c0 = publicInputs.slice(COMMITMENTS_OFFSET, COMMITMENTS_OFFSET + 32);
4672
- const c1 = publicInputs.slice(COMMITMENTS_OFFSET + 32, COMMITMENTS_OFFSET + 64);
4673
- commitments.push(c0);
4674
- commitments.push(c1);
4675
- return commitments;
4676
- }
4677
- if (tag === DEPOSIT_TAG) {
4678
- if (data.length < 1 + 8 + 32) return [];
4679
- const commitment = data.slice(1 + 8, 1 + 8 + 32);
4680
- return [commitment];
4681
- }
4682
- return [];
4792
+ if (tag !== TRANSACT_TAG && tag !== TRANSACT_SWAP_TAG) return [];
4793
+ const publicInputsStart = 1 + PROOF_LEN;
4794
+ const publicInputsEnd = publicInputsStart + PUBLIC_INPUTS_LEN;
4795
+ if (data.length < publicInputsEnd) return [];
4796
+ const publicInputs = data.slice(publicInputsStart, publicInputsEnd);
4797
+ const c0 = publicInputs.slice(COMMITMENTS_OFFSET, COMMITMENTS_OFFSET + 32);
4798
+ const c1 = publicInputs.slice(
4799
+ COMMITMENTS_OFFSET + 32,
4800
+ COMMITMENTS_OFFSET + 64
4801
+ );
4802
+ const out = [];
4803
+ if (!isAllZero(c0)) out.push(c0);
4804
+ if (!isAllZero(c1)) out.push(c1);
4805
+ return out;
4683
4806
  }
4684
4807
  function isBrowser() {
4685
4808
  return typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
@@ -6299,6 +6422,7 @@ async function transact(params, options) {
6299
6422
  if (outputUtxos.length > 2) {
6300
6423
  throw new Error("Maximum 2 output UTXOs allowed");
6301
6424
  }
6425
+ await preflightNullifiers(inputUtxos, connection, programId);
6302
6426
  const mint = inputUtxos.find((u) => u.amount > 0)?.mintAddress ?? outputUtxos.find((u) => u.amount > 0)?.mintAddress ?? NATIVE_SOL_MINT;
6303
6427
  const paddedInputs = [...inputUtxos];
6304
6428
  while (paddedInputs.length < 2) {
@@ -7126,10 +7250,46 @@ async function transact(params, options) {
7126
7250
  }
7127
7251
  }
7128
7252
  if (lastError) {
7253
+ const classified = classifyRelayError(lastError.message);
7254
+ const isMerkleClass = !(classified instanceof UtxoAlreadySpentError) && !(classified instanceof SanctionsQuoteError) && !isRootNotFoundError2(lastError);
7255
+ if (!IS_BROWSER2 && isMerkleClass && relayUrl) {
7256
+ onProgress?.(
7257
+ "All relay-tree attempts failed. Rebuilding merkle tree from chain as last resort..."
7258
+ );
7259
+ try {
7260
+ const [merkleTreePda] = PublicKey6.findProgramAddressSync(
7261
+ [Buffer.from("merkle_tree"), mint.toBuffer()],
7262
+ programId
7263
+ );
7264
+ const chainTree = await buildMerkleTreeFromChain(
7265
+ connection,
7266
+ programId,
7267
+ merkleTreePda,
7268
+ onProgress
7269
+ );
7270
+ const chainRootHex = chainTree.root().toString(16).padStart(64, "0");
7271
+ onProgress?.(
7272
+ `Chain-rebuilt tree root: ${chainRootHex.substring(0, 16)}... \u2014 attached to error for caller retry`
7273
+ );
7274
+ throw new RelayInternalError(
7275
+ `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}`,
7276
+ lastError.message,
7277
+ void 0,
7278
+ chainTree
7279
+ );
7280
+ } catch (e) {
7281
+ if (e instanceof RelayInternalError) throw e;
7282
+ onProgress?.(
7283
+ `Chain-replay fallback failed: ${e instanceof Error ? e.message : String(e)}`
7284
+ );
7285
+ }
7286
+ }
7129
7287
  if (isRootNotFoundError2(lastError)) {
7130
- throw new Error(`Transaction failed after ${maxRootRetries + 1} retries: ${lastError.message}`);
7288
+ throw new Error(
7289
+ `Transaction failed after ${maxRootRetries + 1} retries: ${lastError.message}`
7290
+ );
7131
7291
  }
7132
- throw new Error(`Transaction failed: ${lastError.message}`);
7292
+ throw classified;
7133
7293
  }
7134
7294
  if (relayUrl && outputCommitments.length >= 2) {
7135
7295
  const hasOnChainIndices = commitmentIndices[0] >= 0 && commitmentIndices[1] >= 0;
@@ -7433,6 +7593,7 @@ async function swapUtxo(params, options) {
7433
7593
  if (inputUtxos.length > 2) {
7434
7594
  throw new Error("Maximum 2 input UTXOs allowed");
7435
7595
  }
7596
+ await preflightNullifiers(inputUtxos, connection, programId);
7436
7597
  const outputMintAccount = await connection.getAccountInfo(outputMint, {
7437
7598
  commitment: "confirmed"
7438
7599
  });
@@ -9257,7 +9418,7 @@ var SimpleWallet = class {
9257
9418
  };
9258
9419
 
9259
9420
  // src/index.ts
9260
- var VERSION = "1.0.0";
9421
+ var VERSION = "0.1.5";
9261
9422
  var SCANNER_SUPPORTS_TRANSACT_SWAP = true;
9262
9423
  export {
9263
9424
  CLOAK_PROGRAM_ID,
@@ -9274,12 +9435,15 @@ export {
9274
9435
  MemoryStorageAdapter,
9275
9436
  MerkleTree,
9276
9437
  NATIVE_SOL_MINT,
9438
+ RelayInternalError,
9277
9439
  RelayService,
9278
9440
  RootNotFoundError,
9279
9441
  SCANNER_SUPPORTS_TRANSACT_SWAP,
9280
9442
  SIGN_IN_MESSAGE,
9443
+ SanctionsQuoteError,
9281
9444
  ShieldPoolErrors,
9282
9445
  SimpleWallet,
9446
+ UtxoAlreadySpentError,
9283
9447
  UtxoWallet,
9284
9448
  VARIABLE_FEE_DENOMINATOR,
9285
9449
  VARIABLE_FEE_NUMERATOR,
@@ -9298,6 +9462,7 @@ export {
9298
9462
  calculateRelayFee,
9299
9463
  chainNoteFromBase64,
9300
9464
  chainNoteToBase64,
9465
+ classifyRelayError,
9301
9466
  cleanupStalePendingOperations,
9302
9467
  clearPendingDeposits,
9303
9468
  clearPendingWithdrawals,
@@ -9410,6 +9575,7 @@ export {
9410
9575
  partialWithdraw,
9411
9576
  poseidonHash,
9412
9577
  preflightCheck,
9578
+ preflightNullifiers,
9413
9579
  prepareEncryptedOutput,
9414
9580
  prepareEncryptedOutputForRecipient,
9415
9581
  proofToBytes,
@@ -9457,6 +9623,7 @@ export {
9457
9623
  validateWithdrawableNote,
9458
9624
  verifyAllCircuits,
9459
9625
  verifyCircuitIntegrity,
9626
+ verifyUtxos,
9460
9627
  waitForRoot,
9461
9628
  withTiming
9462
9629
  };
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.1.4",
4
+ "version": "0.1.5",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.js",