@cloak.dev/sdk 0.2.2 → 0.2.3-staging.16d1078

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}$/;
@@ -2150,8 +2150,8 @@ async function verifyUtxos(utxos, connection, programId, commitment = "confirmed
2150
2150
  const nullifierHex = nullifierBig.toString(16).padStart(64, "0");
2151
2151
  const nullifierBytes = Buffer.from(nullifierHex, "hex");
2152
2152
  const { pool } = getShieldPoolPDAs(programId, utxo.mintAddress);
2153
- const [pda] = getNullifierPDA(pool, nullifierBytes, programId);
2154
- checkable.push({ utxo, pda });
2153
+ const [pda2] = getNullifierPDA(pool, nullifierBytes, programId);
2154
+ checkable.push({ utxo, pda: pda2 });
2155
2155
  } catch {
2156
2156
  skipped.push(utxo);
2157
2157
  }
@@ -2221,8 +2221,8 @@ function deriveInputNullifierPdas(programId, mint, inputNullifiers) {
2221
2221
  const pdas = [];
2222
2222
  for (const nullifier of inputNullifiers) {
2223
2223
  if (nullifier === BigInt(0)) continue;
2224
- const [pda] = getNullifierPDA(pool, bigintToBytes324(nullifier), programId);
2225
- pdas.push(pda);
2224
+ const [pda2] = getNullifierPDA(pool, bigintToBytes324(nullifier), programId);
2225
+ pdas.push(pda2);
2226
2226
  }
2227
2227
  return pdas;
2228
2228
  }
@@ -3543,7 +3543,7 @@ async function fetchWithRetry(url, options = {}) {
3543
3543
  const {
3544
3544
  timeoutMs = DEFAULT_TIMEOUT_MS,
3545
3545
  maxRetries = DEFAULT_MAX_RETRIES,
3546
- retryDelayMs = 1e3
3546
+ retryDelayMs: retryDelayMs2 = 1e3
3547
3547
  } = options;
3548
3548
  assertAllowedRelayOrigin(url);
3549
3549
  let lastError = null;
@@ -3567,7 +3567,7 @@ async function fetchWithRetry(url, options = {}) {
3567
3567
  `Relay request failed after ${attempt + 1} attempts: ${lastError.message}`
3568
3568
  );
3569
3569
  }
3570
- const delay = retryDelayMs * Math.pow(2, attempt) + Math.random() * 500;
3570
+ const delay = retryDelayMs2 * Math.pow(2, attempt) + Math.random() * 500;
3571
3571
  await new Promise((resolve) => setTimeout(resolve, delay));
3572
3572
  }
3573
3573
  }
@@ -4778,12 +4778,12 @@ function signRelayRequest(endpoint, programId, body, signer, nowSeconds, fields
4778
4778
  auth_signature: Buffer.from(signature).toString("base64")
4779
4779
  };
4780
4780
  }
4781
- function assertApprovalWithinFreshnessWindow(elapsedMs) {
4781
+ function assertApprovalWithinFreshnessWindow(elapsedMs, maxAgeSeconds = REQUEST_AUTH_MAX_AGE_SECONDS) {
4782
4782
  const elapsedSeconds = elapsedMs / 1e3;
4783
- const budget = REQUEST_AUTH_MAX_AGE_SECONDS - RELAY_AUTH_APPROVAL_MARGIN_SECONDS;
4783
+ const budget = maxAgeSeconds - RELAY_AUTH_APPROVAL_MARGIN_SECONDS;
4784
4784
  if (elapsedSeconds <= budget) return;
4785
4785
  throw new Error(
4786
- `The wallet approval took ${Math.round(elapsedSeconds)} seconds, and Cloak requests stay valid for ${REQUEST_AUTH_MAX_AGE_SECONDS} seconds from the moment they are signed (${RELAY_AUTH_APPROVAL_MARGIN_SECONDS} of those are held back for the request to reach the network). The timestamp is inside the signature, so it cannot be refreshed without your approval again. NOTHING WAS SUBMITTED and no funds moved. Start the same operation again and approve the prompt when it appears.`
4786
+ `The wallet approval took ${Math.round(elapsedSeconds)} seconds, and Cloak requests stay valid for ${maxAgeSeconds} seconds from the moment they are signed (${RELAY_AUTH_APPROVAL_MARGIN_SECONDS} of those are held back for the request to reach the network). The timestamp is inside the signature, so it cannot be refreshed without your approval again. NOTHING WAS SUBMITTED and no funds moved. Start the same operation again and approve the prompt when it appears.`
4787
4787
  );
4788
4788
  }
4789
4789
  function explainRelayAuthRejection(responseText) {
@@ -4841,6 +4841,16 @@ async function buildRelayAuthFields(endpoint, programId, body, signers, fields =
4841
4841
  auth_signature: Buffer.from(signature).toString("base64")
4842
4842
  };
4843
4843
  }
4844
+ var REQUEST_AUTH_BATCH_DOMAIN = "CLOAK_RELAY_BATCH_AUTH_V1";
4845
+ var REQUEST_AUTH_BATCH_MAX_AGE_SECONDS = 600;
4846
+ var RELAY_BATCH_AUTH_MAX_ITEMS = 64;
4847
+ function relayRequestDigestHex(preimage) {
4848
+ return bytesToHex3(sha2563(preimage.message));
4849
+ }
4850
+ function buildRelayBatchAuthMessage(programId, issuedAt, digests) {
4851
+ const lines = [REQUEST_AUTH_BATCH_DOMAIN, programId.toBase58(), issuedAt, String(digests.length)];
4852
+ return new TextEncoder().encode([...lines, ...digests].join("\n"));
4853
+ }
4844
4854
 
4845
4855
  // src/proving/artifacts.ts
4846
4856
  import { sha256 as sha2564 } from "@noble/hashes/sha2";
@@ -5117,6 +5127,7 @@ async function resolveTransactionCircuitFiles() {
5117
5127
  }
5118
5128
  var _transactPaddingSaltCounter = 0;
5119
5129
  var _registeredViewingKeys = /* @__PURE__ */ new Set();
5130
+ var _viewingKeyRegistrationsInFlight = /* @__PURE__ */ new Map();
5120
5131
  var _resolvedAltAccountsCache = /* @__PURE__ */ new Map();
5121
5132
  var _relayMerkleDisabledMints = /* @__PURE__ */ new Set();
5122
5133
  var _relayMerkleFallbackNotifiedMints = /* @__PURE__ */ new Set();
@@ -5449,6 +5460,23 @@ async function computeSwapExtDataHash(outputMint, recipientAta, minOutputAmount,
5449
5460
  ]);
5450
5461
  return poseidonHasher.F.toObject(hash);
5451
5462
  }
5463
+ function assertInputMints(inputUtxos, expectedMint) {
5464
+ const funded = inputUtxos.filter((u) => u.amount > BigInt(0));
5465
+ if (funded.length === 0) return;
5466
+ const first = funded[0].mintAddress;
5467
+ for (const utxo of funded) {
5468
+ if (!utxo.mintAddress.equals(first)) {
5469
+ throw new Error(
5470
+ `Input notes span more than one mint (${first.toBase58()} and ${utxo.mintAddress.toBase58()}). Every pool holds a single mint, so this cannot be a valid spend of either. Select notes of one mint.`
5471
+ );
5472
+ }
5473
+ }
5474
+ if (expectedMint && !first.equals(expectedMint)) {
5475
+ throw new Error(
5476
+ `Input notes are ${first.toBase58()} but this transaction expects ${expectedMint.toBase58()}. The notes decide which pool is spent, so proceeding would move the wrong asset \u2014 refusing. This usually means note selection ignored the mint.`
5477
+ );
5478
+ }
5479
+ }
5452
5480
  function parseNkInput(input) {
5453
5481
  if (typeof input !== "string") {
5454
5482
  if (input.length !== 32) {
@@ -5504,6 +5532,26 @@ async function ensureViewingKeyRegistered(options, onProgress, fallbackNk) {
5504
5532
  if (_registeredViewingKeys.has(cacheKey)) {
5505
5533
  return;
5506
5534
  }
5535
+ const inFlight = _viewingKeyRegistrationsInFlight.get(cacheKey);
5536
+ if (inFlight) {
5537
+ return inFlight;
5538
+ }
5539
+ const registration = registerViewingKeyOnce(
5540
+ relayUrl,
5541
+ userPubkey,
5542
+ viewingKeyHex,
5543
+ cacheKey,
5544
+ options,
5545
+ onProgress
5546
+ );
5547
+ _viewingKeyRegistrationsInFlight.set(cacheKey, registration);
5548
+ try {
5549
+ await registration;
5550
+ } finally {
5551
+ _viewingKeyRegistrationsInFlight.delete(cacheKey);
5552
+ }
5553
+ }
5554
+ async function registerViewingKeyOnce(relayUrl, userPubkey, viewingKeyHex, cacheKey, options, onProgress) {
5507
5555
  const challengeResponse = await relayFetch(`${relayUrl}/viewing-key/challenge`, {
5508
5556
  method: "POST",
5509
5557
  headers: { "Content-Type": "application/json" },
@@ -6859,12 +6907,12 @@ async function submitTransactToRelay(args) {
6859
6907
  const doFetch = args.fetchImpl ?? fetch;
6860
6908
  const maxNetworkRetries = args.maxNetworkRetries ?? relayNetworkRetries();
6861
6909
  const requestTimeoutMs = args.requestTimeoutMs ?? relayRequestTimeoutMs();
6862
- if (!depositorKeypair && args.relayAuthSigner) {
6910
+ if (!depositorKeypair && args.relayAuthSigner && !args.relayAuthBatch) {
6863
6911
  onProgress?.(
6864
6912
  "Approve the request in your wallet. This signs the request for the relay, not a transaction, and it must be approved within a few minutes to stay valid."
6865
6913
  );
6866
6914
  }
6867
- const authFields = await buildRelayAuthFields("/transact", programId, requestBody, {
6915
+ const authFields = args.relayAuthBatch ? await args.relayAuthBatch.authorize("/transact", programId, requestBody, TRANSACT_AUTH_FIELDS) : await buildRelayAuthFields("/transact", programId, requestBody, {
6868
6916
  depositorKeypair,
6869
6917
  relayAuthSigner: args.relayAuthSigner
6870
6918
  });
@@ -7060,7 +7108,7 @@ async function transact(params, options) {
7060
7108
  // names is refused.
7061
7109
  maxRootRetries = 40,
7062
7110
  // Increased to handle prolonged relay sync recovery under high concurrency
7063
- retryDelayMs = 500,
7111
+ retryDelayMs: retryDelayMs2 = 500,
7064
7112
  // Start with short delay, will exponentially back off
7065
7113
  riskOracleQueue,
7066
7114
  riskQuoteUrl: riskQuoteUrlOption,
@@ -7115,6 +7163,7 @@ async function transact(params, options) {
7115
7163
  }
7116
7164
  }
7117
7165
  assertAllowedRpcConnection(connection);
7166
+ assertInputMints(inputUtxos, options.expectedMint);
7118
7167
  await ensureViewingKeyRegistered(options, onProgress, fallbackNk);
7119
7168
  const resolvedChainNoteNk = explicitChainNoteNk ?? fallbackNk ?? null;
7120
7169
  if (inputUtxos.length > 2) {
@@ -7481,7 +7530,7 @@ async function transact(params, options) {
7481
7530
  while (submissionAttempts <= maxRootRetries) {
7482
7531
  const isRetry = submissionAttempts > 0;
7483
7532
  if (isRetry) {
7484
- const exponentialDelay = Math.min(retryDelayMs * Math.pow(1.5, submissionAttempts - 1), 5e3);
7533
+ const exponentialDelay = Math.min(retryDelayMs2 * Math.pow(1.5, submissionAttempts - 1), 5e3);
7485
7534
  const jitter = Math.random() * 500;
7486
7535
  const totalDelay = Math.floor(exponentialDelay + jitter);
7487
7536
  await sleep2(totalDelay);
@@ -7620,11 +7669,14 @@ async function transact(params, options) {
7620
7669
  }
7621
7670
  let proof;
7622
7671
  let publicSignals;
7672
+ const releaseProofSlot = options.relayAuthBatch ? await options.relayAuthBatch.proofSlot() : void 0;
7623
7673
  try {
7624
7674
  const result = await generateTransactionProof(circuitInputs, onProofProgress);
7625
7675
  proof = result.proof;
7626
7676
  publicSignals = result.publicSignals;
7677
+ releaseProofSlot?.();
7627
7678
  } catch (proofError) {
7679
+ releaseProofSlot?.();
7628
7680
  const errorMsg = proofError?.message || String(proofError);
7629
7681
  if ((errorMsg.includes("ForceEqual") || errorMsg.includes("Assert Failed")) && submissionAttempts < maxRootRetries) {
7630
7682
  onProgress?.(`Merkle proof error (stale sibling data), will regenerate with fresh tree...`);
@@ -7644,7 +7696,7 @@ async function transact(params, options) {
7644
7696
  }
7645
7697
  treeState = null;
7646
7698
  useSiblingInfo = false;
7647
- const waitMs = Math.min(retryDelayMs * Math.pow(2, Math.min(submissionAttempts, 4)), 5e3);
7699
+ const waitMs = Math.min(retryDelayMs2 * Math.pow(2, Math.min(submissionAttempts, 4)), 5e3);
7648
7700
  if (waitMs > 0) {
7649
7701
  onProgress?.(`Waiting ${waitMs}ms for relay sync before retry...`);
7650
7702
  await new Promise((r) => setTimeout(r, waitMs));
@@ -7937,6 +7989,7 @@ async function transact(params, options) {
7937
7989
  // point where they reach the relay submission instead of stopping at viewing-key
7938
7990
  // registration. The keypair still wins when both are present.
7939
7991
  relayAuthSigner: relayAuthPlan.kind === "wallet" ? relayAuthPlan.signer : void 0,
7992
+ relayAuthBatch: options.relayAuthBatch,
7940
7993
  settlement: {
7941
7994
  connection,
7942
7995
  programId,
@@ -8338,7 +8391,7 @@ async function swapUtxo(params, options) {
8338
8391
  riskQuoteUrl: riskQuoteUrlOption,
8339
8392
  maxRootRetries = 40,
8340
8393
  // Increased to handle prolonged relay sync recovery under high concurrency
8341
- retryDelayMs = 500,
8394
+ retryDelayMs: retryDelayMs2 = 500,
8342
8395
  // Start with short delay, will exponentially back off
8343
8396
  useUniqueNullifiers,
8344
8397
  useChainRootForProof = true
@@ -8357,6 +8410,12 @@ async function swapUtxo(params, options) {
8357
8410
  if (inputUtxos.length > 2) {
8358
8411
  throw new Error("Maximum 2 input UTXOs allowed");
8359
8412
  }
8413
+ assertInputMints(inputUtxos, NATIVE_SOL_MINT);
8414
+ if (options.expectedMint && !options.expectedMint.equals(NATIVE_SOL_MINT)) {
8415
+ throw new Error(
8416
+ `swapUtxo expects ${NATIVE_SOL_MINT.toBase58()} input notes (a swap always spends from the wSOL pool), but expectedMint was ${options.expectedMint.toBase58()}. Drop expectedMint, or spend the other mint with transact/transfer instead.`
8417
+ );
8418
+ }
8360
8419
  await preflightNullifiers(inputUtxos, connection, programId);
8361
8420
  const outputMintAccount = await connection.getAccountInfo(outputMint, {
8362
8421
  commitment: "confirmed"
@@ -8743,7 +8802,7 @@ async function swapUtxo(params, options) {
8743
8802
  let walletApprovals = 0;
8744
8803
  for (let attempt = 0; attempt <= maxRootRetries; attempt++) {
8745
8804
  if (attempt > 0) {
8746
- const exponentialDelay = Math.min(retryDelayMs * Math.pow(1.5, attempt - 1), 5e3);
8805
+ const exponentialDelay = Math.min(retryDelayMs2 * Math.pow(1.5, attempt - 1), 5e3);
8747
8806
  const jitter = Math.random() * 500;
8748
8807
  const totalDelay = Math.floor(exponentialDelay + jitter);
8749
8808
  await sleep2(totalDelay);
@@ -8881,7 +8940,7 @@ async function swapUtxo(params, options) {
8881
8940
  }
8882
8941
  merkleState = null;
8883
8942
  useSiblingInfo = false;
8884
- const waitMs = Math.min(retryDelayMs * Math.pow(2, Math.min(attempt, 4)), 5e3);
8943
+ const waitMs = Math.min(retryDelayMs2 * Math.pow(2, Math.min(attempt, 4)), 5e3);
8885
8944
  if (waitMs > 0) {
8886
8945
  onProgress?.(`Waiting ${waitMs}ms for relay sync before retry...`);
8887
8946
  await new Promise((r) => setTimeout(r, waitMs));
@@ -9383,10 +9442,273 @@ function cleanupStalePendingOperations(maxAgeMs = 24 * 60 * 60 * 1e3) {
9383
9442
  return { removedDeposits, removedWithdrawals };
9384
9443
  }
9385
9444
 
9445
+ // src/relay/batch-auth.ts
9446
+ import { Keypair as Keypair3 } from "@solana/web3.js";
9447
+ import nacl7 from "tweetnacl";
9448
+ var RelayBatchAuthCoordinator = class {
9449
+ constructor(options) {
9450
+ this.proofSlotsInUse = 0;
9451
+ this.proofQueue = [];
9452
+ this.items = [];
9453
+ this.signing = false;
9454
+ this.slotsInUse = 0;
9455
+ this.ready = [];
9456
+ this.waves = 0;
9457
+ if (!Number.isInteger(options.items) || options.items < 1) {
9458
+ throw new Error(`A batch needs at least one item (got ${options.items}).`);
9459
+ }
9460
+ if (options.items > RELAY_BATCH_AUTH_MAX_ITEMS) {
9461
+ throw new Error(
9462
+ `A batch may cover at most ${RELAY_BATCH_AUTH_MAX_ITEMS} requests (got ${options.items}); the relay refuses longer digest lists. Chunk the payout. The practical bound is lower still: each spend appends two roots to the on-chain ring of 100, so a batch proved against one root evicts it by itself before 50 items.`
9463
+ );
9464
+ }
9465
+ const concurrency = options.submitConcurrency ?? 3;
9466
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
9467
+ throw new Error(`submitConcurrency must be a positive integer (got ${concurrency}).`);
9468
+ }
9469
+ this.signer = options.signer;
9470
+ this.sender = options.signer instanceof Keypair3 ? options.signer.publicKey : options.signer.walletPublicKey;
9471
+ this.expectedItems = options.items;
9472
+ this.submitConcurrency = concurrency;
9473
+ const proofConcurrency = options.proofConcurrency ?? 2;
9474
+ if (!Number.isInteger(proofConcurrency) || proofConcurrency < 1) {
9475
+ throw new Error(`proofConcurrency must be a positive integer (got ${proofConcurrency}).`);
9476
+ }
9477
+ this.proofConcurrency = proofConcurrency;
9478
+ this.onProgress = options.onProgress;
9479
+ }
9480
+ /** The wallet that signs and is every item's authenticated `sender`. */
9481
+ get walletPublicKey() {
9482
+ return this.sender;
9483
+ }
9484
+ /** How many times the signer has been asked so far: one per wave. */
9485
+ get approvals() {
9486
+ return this.waves;
9487
+ }
9488
+ /** Where every item is right now. For progress UIs and for diagnosing a batch that never signs. */
9489
+ snapshot() {
9490
+ const count = (state) => this.items.filter((i) => i.state === state).length;
9491
+ return {
9492
+ building: count("building"),
9493
+ waiting: count("waiting"),
9494
+ submitting: count("submitting"),
9495
+ done: count("done"),
9496
+ proving: this.proofSlotsInUse,
9497
+ proofQueue: this.proofQueue.length,
9498
+ registered: this.items.length,
9499
+ expected: this.expectedItems
9500
+ };
9501
+ }
9502
+ /** Register one item. Throws past `items`: the wave condition would never be reachable. */
9503
+ item() {
9504
+ if (this.items.length >= this.expectedItems) {
9505
+ throw new Error(
9506
+ `This batch was created for ${this.expectedItems} item(s) and all of them are registered.`
9507
+ );
9508
+ }
9509
+ const item = { state: "building", waiting: null, holdsSlot: false };
9510
+ this.items.push(item);
9511
+ return {
9512
+ proofSlot: () => this.acquireProofSlot(),
9513
+ authorize: (endpoint, programId, body, fields) => this.authorize(item, endpoint, programId, body, fields),
9514
+ finish: () => this.finish(item)
9515
+ };
9516
+ }
9517
+ acquireProofSlot() {
9518
+ return new Promise((resolve) => {
9519
+ const grant = () => {
9520
+ this.proofSlotsInUse += 1;
9521
+ let released = false;
9522
+ resolve(() => {
9523
+ if (released) return;
9524
+ released = true;
9525
+ this.proofSlotsInUse -= 1;
9526
+ const next = this.proofQueue.shift();
9527
+ if (next) next();
9528
+ });
9529
+ };
9530
+ if (this.proofSlotsInUse < this.proofConcurrency) grant();
9531
+ else this.proofQueue.push(grant);
9532
+ });
9533
+ }
9534
+ authorize(item, endpoint, programId, body, fields) {
9535
+ if (item.state === "done") {
9536
+ return Promise.reject(new Error("This batch item already finished; it cannot authorize."));
9537
+ }
9538
+ if (item.state === "waiting") {
9539
+ return Promise.reject(new Error("This batch item is already waiting for a signature."));
9540
+ }
9541
+ this.releaseSlot(item);
9542
+ return new Promise((resolve, reject) => {
9543
+ item.state = "waiting";
9544
+ item.waiting = { endpoint, programId, body, fields, resolve, reject };
9545
+ this.maybeSign();
9546
+ });
9547
+ }
9548
+ finish(item) {
9549
+ if (item.state === "done") return;
9550
+ if (item.state === "waiting" && item.waiting) {
9551
+ item.waiting.reject(new Error("Batch item finished before it was signed."));
9552
+ item.waiting = null;
9553
+ }
9554
+ this.releaseSlot(item);
9555
+ item.state = "done";
9556
+ this.maybeSign();
9557
+ this.pump();
9558
+ }
9559
+ releaseSlot(item) {
9560
+ if (!item.holdsSlot) return;
9561
+ item.holdsSlot = false;
9562
+ this.slotsInUse -= 1;
9563
+ }
9564
+ /** Sign when every registered item is either waiting here or finished, and nothing is building. */
9565
+ maybeSign() {
9566
+ if (this.signing) return;
9567
+ if (this.items.length < this.expectedItems) return;
9568
+ if (this.items.some((item) => item.state === "building" || item.state === "submitting")) return;
9569
+ const wave = this.items.filter((item) => item.state === "waiting");
9570
+ if (wave.length === 0) return;
9571
+ this.signing = true;
9572
+ void this.signWave(wave).finally(() => {
9573
+ this.signing = false;
9574
+ this.maybeSign();
9575
+ });
9576
+ }
9577
+ async signWave(wave) {
9578
+ const waiting = wave.map((item) => item.waiting);
9579
+ try {
9580
+ const programId = waiting[0].programId;
9581
+ for (const w of waiting) {
9582
+ if (!w.programId.equals(programId)) {
9583
+ throw new Error("Every item of a batch must target the same program id.");
9584
+ }
9585
+ }
9586
+ const nowSeconds = Math.floor(Date.now() / 1e3);
9587
+ const preimages = waiting.map(
9588
+ (w) => buildRelayAuthPreimage(w.endpoint, programId, w.body, this.sender, nowSeconds, w.fields)
9589
+ );
9590
+ const digests = preimages.map(relayRequestDigestHex);
9591
+ const issuedAt = preimages[0].auth_issued_at;
9592
+ const message = buildRelayBatchAuthMessage(programId, issuedAt, digests);
9593
+ this.waves += 1;
9594
+ let signature;
9595
+ if (this.signer instanceof Keypair3) {
9596
+ signature = nacl7.sign.detached(message, this.signer.secretKey);
9597
+ } else {
9598
+ this.onProgress?.(
9599
+ `Approve ${waiting.length} request(s) in your wallet` + (this.waves > 1 ? ` (approval ${this.waves}, for the rows that had to re-prove)` : "") + `. This signs the requests for the relay, not a transaction, and must be approved within a few minutes to stay valid.`
9600
+ );
9601
+ const approvalStartedMs = Date.now();
9602
+ signature = await this.signer.signMessage(message);
9603
+ assertApprovalWithinFreshnessWindow(
9604
+ Date.now() - approvalStartedMs,
9605
+ REQUEST_AUTH_BATCH_MAX_AGE_SECONDS
9606
+ );
9607
+ if (!(signature instanceof Uint8Array) || signature.length !== 64) {
9608
+ throw new Error(
9609
+ `The wallet's signMessage did not return a 64-byte ed25519 detached signature (got ${signature instanceof Uint8Array ? `${signature.length} bytes` : typeof signature}).`
9610
+ );
9611
+ }
9612
+ }
9613
+ const signatureB64 = Buffer.from(signature).toString("base64");
9614
+ wave.forEach((item, i) => {
9615
+ const preimage = preimages[i];
9616
+ this.ready.push({
9617
+ item,
9618
+ fields: {
9619
+ sender: preimage.sender,
9620
+ auth_issued_at: preimage.auth_issued_at,
9621
+ auth_nonce: preimage.auth_nonce,
9622
+ auth_signature: signatureB64,
9623
+ // A fresh array per item: `Object.assign` puts this on the wire body, and the body is
9624
+ // serialized again on every network retry.
9625
+ auth_batch: { digests: [...digests] }
9626
+ }
9627
+ });
9628
+ });
9629
+ this.pump();
9630
+ } catch (error) {
9631
+ for (const item of wave) {
9632
+ const w = item.waiting;
9633
+ item.waiting = null;
9634
+ item.state = "done";
9635
+ w?.reject(error);
9636
+ }
9637
+ }
9638
+ }
9639
+ /** Release signed items to submit, `submitConcurrency` at a time, in signing order. */
9640
+ pump() {
9641
+ while (this.ready.length > 0 && this.slotsInUse < this.submitConcurrency) {
9642
+ const next = this.ready.shift();
9643
+ const w = next.item.waiting;
9644
+ next.item.waiting = null;
9645
+ if (next.item.state !== "waiting" || !w) continue;
9646
+ next.item.state = "submitting";
9647
+ next.item.holdsSlot = true;
9648
+ this.slotsInUse += 1;
9649
+ w.resolve(next.fields);
9650
+ }
9651
+ }
9652
+ };
9653
+ function createRelayBatchAuthCoordinator(options) {
9654
+ return new RelayBatchAuthCoordinator(options);
9655
+ }
9656
+
9657
+ // src/flows/transact-batch.ts
9658
+ async function transactBatch(items, options) {
9659
+ if (items.length === 0) {
9660
+ return { results: [], approvals: 0 };
9661
+ }
9662
+ if (items.length > RELAY_BATCH_AUTH_MAX_ITEMS) {
9663
+ throw new Error(
9664
+ `transactBatch accepts at most ${RELAY_BATCH_AUTH_MAX_ITEMS} items per call (got ${items.length}). Chunk the payout; see RELAY_BATCH_AUTH_MAX_ITEMS for why the practical bound is lower.`
9665
+ );
9666
+ }
9667
+ if (!options.relayUrl) {
9668
+ throw new Error("transactBatch requires relayUrl: batch approval is a relay authentication scheme.");
9669
+ }
9670
+ const { proofConcurrency, submitConcurrency, ...shared } = options;
9671
+ const signer = shared.depositorKeypair ? shared.depositorKeypair : shared.walletPublicKey && shared.signMessage ? { walletPublicKey: shared.walletPublicKey, signMessage: shared.signMessage } : shared.depositorPublicKey && shared.signMessage ? { walletPublicKey: shared.depositorPublicKey, signMessage: shared.signMessage } : null;
9672
+ if (!signer) {
9673
+ throw new Error(
9674
+ "transactBatch requires an authenticated sender: pass `depositorKeypair` (a local Keypair) or `signMessage` together with `walletPublicKey` (a browser wallet adapter). Checked before proof generation, so nothing has been computed or submitted yet."
9675
+ );
9676
+ }
9677
+ const coordinator = createRelayBatchAuthCoordinator({
9678
+ signer,
9679
+ items: items.length,
9680
+ proofConcurrency,
9681
+ submitConcurrency,
9682
+ onProgress: shared.onProgress
9683
+ });
9684
+ const handles = items.map(() => coordinator.item());
9685
+ const results = new Array(items.length);
9686
+ const runOne = async (index) => {
9687
+ const item = items[index];
9688
+ const handle = handles[index];
9689
+ const label = `[batch ${index + 1}/${items.length}]`;
9690
+ try {
9691
+ const value = await transact(item.params, {
9692
+ ...shared,
9693
+ ...item.options,
9694
+ relayAuthBatch: handle,
9695
+ onProgress: shared.onProgress ? (status) => shared.onProgress?.(`${label} ${status}`) : void 0
9696
+ });
9697
+ results[index] = { status: "fulfilled", value };
9698
+ } catch (reason) {
9699
+ results[index] = { status: "rejected", reason };
9700
+ } finally {
9701
+ handle.finish();
9702
+ }
9703
+ };
9704
+ await Promise.all(items.map((_, index) => runOne(index)));
9705
+ return { results, approvals: coordinator.approvals };
9706
+ }
9707
+
9386
9708
  // src/scanning/scan.ts
9387
9709
  import _bs583 from "bs58";
9388
9710
  import {
9389
- PublicKey as PublicKey9
9711
+ PublicKey as PublicKey10
9390
9712
  } from "@solana/web3.js";
9391
9713
  import { getAssociatedTokenAddressSync as getAssociatedTokenAddressSync2 } from "@solana/spl-token";
9392
9714
  var bs583 = _bs583.default || _bs583;
@@ -9474,7 +9796,7 @@ function parseSwapOutputMint(data) {
9474
9796
  const mintBytes = data.slice(SWAP_OUTPUT_MINT_OFFSET, end);
9475
9797
  if (mintBytes.every((byte) => byte === 0)) return void 0;
9476
9798
  try {
9477
- return new PublicKey9(mintBytes).toBase58();
9799
+ return new PublicKey10(mintBytes).toBase58();
9478
9800
  } catch {
9479
9801
  return void 0;
9480
9802
  }
@@ -9564,7 +9886,7 @@ function parseSwapRecipientAta(data) {
9564
9886
  const recipientAtaBytes = data.slice(recipientAtaStart, recipientAtaEnd);
9565
9887
  if (recipientAtaBytes.every((byte) => byte === 0)) return void 0;
9566
9888
  try {
9567
- return new PublicKey9(recipientAtaBytes).toBase58();
9889
+ return new PublicKey10(recipientAtaBytes).toBase58();
9568
9890
  } catch {
9569
9891
  return void 0;
9570
9892
  }
@@ -9726,7 +10048,7 @@ async function scanSwapNoteCarriers(connection, programId, viewingKeyNk, swapCtx
9726
10048
  let rpcCalls = 0;
9727
10049
  const candidates = Array.from(swapCtxByCommitment.keys());
9728
10050
  if (candidates.length === 0) return { records, rpcCalls };
9729
- const [registry] = PublicKey9.findProgramAddressSync(
10051
+ const [registry] = PublicKey10.findProgramAddressSync(
9730
10052
  [Buffer.from("cloak_chain_note_registry")],
9731
10053
  programId
9732
10054
  );
@@ -10129,8 +10451,8 @@ async function scanTransactions(opts) {
10129
10451
  if (onChainAta) {
10130
10452
  try {
10131
10453
  const expectedAta = getAssociatedTokenAddressSync2(
10132
- new PublicKey9(asset.mint),
10133
- new PublicKey9(walletPublicKey)
10454
+ new PublicKey10(asset.mint),
10455
+ new PublicKey10(walletPublicKey)
10134
10456
  ).toBase58();
10135
10457
  if (onChainAta === expectedAta) {
10136
10458
  isOurs = true;
@@ -10344,7 +10666,7 @@ async function scanTransactions(opts) {
10344
10666
  // deposit — that is the public deposit amount. A deposit that also merged inputs
10345
10667
  // carries a larger output 0 and is not recoverable from public data alone.
10346
10668
  amount: grossAmount,
10347
- mintAddress: new PublicKey9(asset.mint),
10669
+ mintAddress: new PublicKey10(asset.mint),
10348
10670
  outputCommitments: ixCtx.outputCommitments ?? []
10349
10671
  });
10350
10672
  if (recoveredDeposit) {
@@ -10370,7 +10692,7 @@ async function scanTransactions(opts) {
10370
10692
  noteSalt: compactNote.noteSalt,
10371
10693
  amount: compactNote.outAmount0,
10372
10694
  keypair: { privateKey: 0n, publicKey: compactNote.outPubkey0 },
10373
- mintAddress: new PublicKey9(asset.mint),
10695
+ mintAddress: new PublicKey10(asset.mint),
10374
10696
  outputIndex: 0,
10375
10697
  // v4 describes output 0, which is where change lands
10376
10698
  outputCommitments: ixCtx.outputCommitments ?? []
@@ -10563,7 +10885,7 @@ function formatComplianceCsv(report) {
10563
10885
  }
10564
10886
 
10565
10887
  // src/wallet/utxo-wallet.ts
10566
- import { PublicKey as PublicKey10 } from "@solana/web3.js";
10888
+ import { PublicKey as PublicKey11 } from "@solana/web3.js";
10567
10889
  var UtxoWallet = class _UtxoWallet {
10568
10890
  constructor(viewingKey) {
10569
10891
  this.wallets = /* @__PURE__ */ new Map();
@@ -10775,7 +11097,7 @@ var UtxoWallet = class _UtxoWallet {
10775
11097
  data.viewingKey ? new Uint8Array(data.viewingKey) : void 0
10776
11098
  );
10777
11099
  for (const w of data.wallets) {
10778
- const mint = new PublicKey10(w.mint);
11100
+ const mint = new PublicKey11(w.mint);
10779
11101
  for (const u of w.utxos) {
10780
11102
  wallet.addUtxo({
10781
11103
  amount: BigInt(u.amount),
@@ -10857,12 +11179,799 @@ var SimpleWallet = class {
10857
11179
  }
10858
11180
  };
10859
11181
 
11182
+ // src/bridge/rail-verify.ts
11183
+ import nacl8 from "tweetnacl";
11184
+ import { sha256 as sha2565 } from "@noble/hashes/sha256";
11185
+ var ONECLICK_PUBKEY_B58 = "reYaWhvwu8Jzo3WUM3zhn6VrhuMEF4eADL17qtRVifc";
11186
+ var b58 = /* @__PURE__ */ (() => {
11187
+ const A = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
11188
+ return {
11189
+ decode(s) {
11190
+ let n = 0n;
11191
+ for (const c of s) {
11192
+ const i = A.indexOf(c);
11193
+ if (i < 0) throw new Error("bad base58");
11194
+ n = n * 58n + BigInt(i);
11195
+ }
11196
+ const bytes = [];
11197
+ while (n > 0n) {
11198
+ bytes.unshift(Number(n & 255n));
11199
+ n >>= 8n;
11200
+ }
11201
+ for (const c of s) {
11202
+ if (c === "1") bytes.unshift(0);
11203
+ else break;
11204
+ }
11205
+ return Uint8Array.from(bytes);
11206
+ },
11207
+ encode(b) {
11208
+ let n = 0n;
11209
+ for (const x of b) n = n * 256n + BigInt(x);
11210
+ let s = "";
11211
+ while (n > 0n) {
11212
+ s = A[Number(n % 58n)] + s;
11213
+ n /= 58n;
11214
+ }
11215
+ for (const x of b) {
11216
+ if (x === 0) s = "1" + s;
11217
+ else break;
11218
+ }
11219
+ return s;
11220
+ }
11221
+ };
11222
+ })();
11223
+ function stable(v) {
11224
+ if (v === null || typeof v !== "object") return JSON.stringify(v) ?? "null";
11225
+ if (Array.isArray(v)) return `[${v.map(stable).join(",")}]`;
11226
+ const o = v;
11227
+ const parts = Object.keys(o).sort().filter((k) => o[k] !== void 0).map((k) => `${JSON.stringify(k)}:${stable(o[k])}`);
11228
+ return `{${parts.join(",")}}`;
11229
+ }
11230
+ function signedRequest(r) {
11231
+ const q = r.quoteRequest ?? {};
11232
+ return {
11233
+ dry: q.dry,
11234
+ swapType: q.swapType,
11235
+ slippageTolerance: q.slippageTolerance,
11236
+ originAsset: q.originAsset,
11237
+ depositType: q.depositType,
11238
+ destinationAsset: q.destinationAsset,
11239
+ amount: q.amount,
11240
+ refundTo: q.refundTo,
11241
+ refundType: q.refundType,
11242
+ recipient: q.recipient,
11243
+ recipientType: q.recipientType,
11244
+ deadline: q.deadline,
11245
+ quoteWaitingTimeMs: q.quoteWaitingTimeMs || void 0,
11246
+ referral: q.referral || void 0,
11247
+ virtualChainRecipient: q.virtualChainRecipient || void 0,
11248
+ virtualChainRefundRecipient: q.virtualChainRefundRecipient || void 0,
11249
+ customRecipientMsg: q.customRecipientMsg || void 0
11250
+ // Deliberately unsigned by the rail: sessionId, connectedWallets, correlationId, appFees,
11251
+ // partnerId, userAccountId, depositMode. APP FEES ARE NOT AUTHENTICATED — never display a fee
11252
+ // caption as if the signature vouched for it.
11253
+ };
11254
+ }
11255
+ function signedQuote(r) {
11256
+ const q = r.quote ?? {};
11257
+ const base = {
11258
+ amountIn: q.amountIn,
11259
+ amountInFormatted: q.amountInFormatted,
11260
+ amountInUsd: q.amountInUsd,
11261
+ minAmountIn: q.minAmountIn,
11262
+ amountOut: q.amountOut,
11263
+ amountOutFormatted: q.amountOutFormatted,
11264
+ amountOutUsd: q.amountOutUsd,
11265
+ minAmountOut: q.minAmountOut
11266
+ };
11267
+ if (r.quoteRequest?.dry) return base;
11268
+ return {
11269
+ ...base,
11270
+ depositAddress: q.depositAddress || void 0,
11271
+ // <- the field that makes substitution detectable
11272
+ depositMemo: q.depositMemo || void 0,
11273
+ deadline: q.deadline || void 0,
11274
+ // <- a real, SIGNED expiry
11275
+ timeWhenInactive: q.timeWhenInactive || void 0,
11276
+ timeEstimate: q.timeEstimate || void 0,
11277
+ virtualChainRecipient: q.virtualChainRecipient || void 0,
11278
+ virtualChainRefundRecipient: q.virtualChainRefundRecipient || void 0,
11279
+ customRecipientMsg: q.customRecipientMsg || void 0,
11280
+ refundFee: q.refundFee || void 0,
11281
+ withdrawFee: q.withdrawFee || void 0
11282
+ };
11283
+ }
11284
+ function canonicalPayloadString(resp) {
11285
+ return stable({ ...signedRequest(resp), ...signedQuote(resp), timestamp: resp.timestamp });
11286
+ }
11287
+ function verifyQuoteSignature(resp) {
11288
+ if (!resp?.signature) return { valid: false, reason: "no signature on the response" };
11289
+ const payload = { ...signedRequest(resp), ...signedQuote(resp), timestamp: resp.timestamp };
11290
+ const digest = sha2565(new TextEncoder().encode(stable(payload)));
11291
+ const message = new TextEncoder().encode(b58.encode(new Uint8Array(digest)));
11292
+ const sig = resp.signature.replace(/^ed25519:/, "");
11293
+ try {
11294
+ return { valid: nacl8.sign.detached.verify(message, b58.decode(sig), b58.decode(ONECLICK_PUBKEY_B58)) };
11295
+ } catch (e) {
11296
+ return { valid: false, reason: e instanceof Error ? e.message : String(e) };
11297
+ }
11298
+ }
11299
+
11300
+ // src/bridge/rails.ts
11301
+ async function parseBridgeResponse(res, what) {
11302
+ let body;
11303
+ try {
11304
+ body = await res.json();
11305
+ } catch {
11306
+ throw new Error(`${what} failed: ${res.status} ${res.statusText} (response body was not JSON)`);
11307
+ }
11308
+ if (!res.ok) {
11309
+ const b = body ?? {};
11310
+ throw new Error(
11311
+ `${what} failed: ${b.message ?? res.statusText} (${b.code ?? res.status}${b.retryable ? ", retryable" : ""})`
11312
+ );
11313
+ }
11314
+ return body;
11315
+ }
11316
+ function toAttestation(raw) {
11317
+ if (raw.kind === "ed25519") {
11318
+ return { kind: "ed25519", verified: raw.verified ?? false, signer: raw.signer ?? "" };
11319
+ }
11320
+ return { kind: "none", checks: raw.checks ?? [], note: raw.note ?? "" };
11321
+ }
11322
+ async function fetchBridgeQuote(relayUrl, req) {
11323
+ const body = {
11324
+ recipient: req.recipient,
11325
+ origin_chain: req.originChain,
11326
+ amount_base_units: req.amountBaseUnits.toString(),
11327
+ allocate: req.allocate ?? false
11328
+ };
11329
+ if (req.refundTo !== void 0) body.refund_to = req.refundTo;
11330
+ if (req.rails !== void 0) body.rails = req.rails;
11331
+ const res = await relayFetch(`${relayUrl}/bridge/quote`, {
11332
+ method: "POST",
11333
+ headers: { "Content-Type": "application/json" },
11334
+ body: JSON.stringify(body)
11335
+ });
11336
+ const raw = await parseBridgeResponse(res, "bridge quote");
11337
+ const options = [];
11338
+ const unavailable = (raw.unavailable ?? []).map((p) => ({
11339
+ rail: p.rail,
11340
+ reason: p.reason
11341
+ }));
11342
+ for (const opt of raw.options) {
11343
+ const attestation = toAttestation(opt.attestation);
11344
+ if (attestation.kind === "ed25519") {
11345
+ const verdict = opt.rail_response ? verifyQuoteSignature(opt.rail_response) : { valid: false, reason: "no rail_response to verify against" };
11346
+ if (!verdict.valid) {
11347
+ unavailable.push({
11348
+ rail: opt.rail,
11349
+ reason: `deposit address failed independent signature verification, refusing to display it (${verdict.reason ?? "signature did not verify"})`
11350
+ });
11351
+ continue;
11352
+ }
11353
+ }
11354
+ options.push({
11355
+ rail: opt.rail,
11356
+ refunds: opt.refunds,
11357
+ addressLifetime: opt.address_lifetime,
11358
+ amountOut: BigInt(opt.amount_out),
11359
+ minAmountOut: BigInt(opt.min_amount_out),
11360
+ timeEstimateSeconds: opt.time_estimate_s,
11361
+ expiresAt: opt.expires_at,
11362
+ depositAddress: opt.deposit_address,
11363
+ attestation
11364
+ });
11365
+ }
11366
+ return { options, unavailable };
11367
+ }
11368
+ var STATUS_STATES = [
11369
+ "pending",
11370
+ "delivered",
11371
+ "refunded",
11372
+ "expired",
11373
+ "unknown"
11374
+ ];
11375
+ async function fetchBridgeStatus(relayUrl, depositAddress, rail) {
11376
+ const qs = new URLSearchParams({ deposit_address: depositAddress, rail });
11377
+ const res = await relayFetch(`${relayUrl}/bridge/status?${qs.toString()}`);
11378
+ const raw = await parseBridgeResponse(res, "bridge status");
11379
+ const state = STATUS_STATES.includes(raw.state ?? "") ? raw.state : "unknown";
11380
+ return { rail: raw.rail ?? rail, state, detail: raw.detail ?? "" };
11381
+ }
11382
+ function cloakBridgeRail(relayUrl) {
11383
+ assertAllowedRelayOrigin(relayUrl);
11384
+ return {
11385
+ id: "cloak-bridge",
11386
+ quote: (req) => fetchBridgeQuote(relayUrl, req),
11387
+ status: (depositAddress, rail) => fetchBridgeStatus(relayUrl, depositAddress, rail)
11388
+ };
11389
+ }
11390
+
11391
+ // src/bridge/paymaster-client.ts
11392
+ import {
11393
+ PublicKey as PublicKey12,
11394
+ SystemInstruction,
11395
+ SystemProgram as SystemProgram3,
11396
+ Transaction as Transaction3
11397
+ } from "@solana/web3.js";
11398
+ import { TOKEN_PROGRAM_ID as TOKEN_PROGRAM_ID3, decodeTransferInstruction, getAssociatedTokenAddressSync as getAssociatedTokenAddressSync3 } from "@solana/spl-token";
11399
+ function validatePaymasterTopUpTransaction(tx, expect) {
11400
+ const ixs = tx.instructions;
11401
+ if (ixs.length !== 1 && ixs.length !== 2) {
11402
+ throw new Error(
11403
+ `paymaster top-up must be exactly 1 or 2 instructions, got ${ixs.length} \u2014 refusing to sign a transaction whose shape does not match the documented top-up contract`
11404
+ );
11405
+ }
11406
+ const ix0 = ixs[0];
11407
+ if (!ix0.programId.equals(SystemProgram3.programId)) {
11408
+ throw new Error(
11409
+ `paymaster top-up instruction 0 is owned by ${ix0.programId.toBase58()}, not the System Program \u2014 refusing to sign an unrecognised first instruction`
11410
+ );
11411
+ }
11412
+ const transfer2 = SystemInstruction.decodeTransfer(ix0);
11413
+ if (!transfer2.toPubkey.equals(expect.recipient)) {
11414
+ throw new Error(
11415
+ `paymaster top-up sends SOL to ${transfer2.toPubkey.toBase58()}, which is not our recipient ${expect.recipient.toBase58()} \u2014 refusing to sign a transaction that funds a key we do not control`
11416
+ );
11417
+ }
11418
+ if (BigInt(transfer2.lamports) > expect.maxGrantLamports) {
11419
+ throw new Error(
11420
+ `paymaster top-up grants ${transfer2.lamports} lamports, more than the ${expect.maxGrantLamports} we asked for \u2014 refusing a transaction that funds beyond our own request`
11421
+ );
11422
+ }
11423
+ if (ixs.length === 1) return;
11424
+ const ix1 = ixs[1];
11425
+ if (!ix1.programId.equals(TOKEN_PROGRAM_ID3)) {
11426
+ throw new Error(
11427
+ `paymaster top-up instruction 1 is owned by ${ix1.programId.toBase58()}, not the SPL Token program \u2014 refusing to sign an unrecognised second instruction`
11428
+ );
11429
+ }
11430
+ const spl = decodeTransferInstruction(ix1, TOKEN_PROGRAM_ID3);
11431
+ const authority = spl.keys.owner.pubkey;
11432
+ if (!authority.equals(expect.recipient)) {
11433
+ throw new Error(
11434
+ `paymaster top-up's fee transfer is authorised by ${authority.toBase58()}, not our recipient ${expect.recipient.toBase58()} \u2014 refusing to sign away tokens from an account we do not control`
11435
+ );
11436
+ }
11437
+ const expectedDestination = getAssociatedTokenAddressSync3(
11438
+ expect.feeMint,
11439
+ expect.paymentAddress,
11440
+ false
11441
+ );
11442
+ const destination = spl.keys.destination.pubkey;
11443
+ if (!destination.equals(expectedDestination)) {
11444
+ throw new Error(
11445
+ `paymaster top-up's fee transfer is addressed to ${destination.toBase58()}, not the paymaster's own payment ATA ${expectedDestination.toBase58()} \u2014 refusing to sign a payment to an unverified destination`
11446
+ );
11447
+ }
11448
+ const paidAmount = BigInt(spl.data.amount);
11449
+ if (paidAmount !== expect.feeTokenAmount) {
11450
+ throw new Error(
11451
+ `paymaster top-up's fee amount is ${paidAmount}, not the ${expect.feeTokenAmount} Kora quoted at prepare time \u2014 refusing to sign a payment that does not match the quote`
11452
+ );
11453
+ }
11454
+ }
11455
+ async function parseBridgeResponse2(res, what) {
11456
+ let body;
11457
+ try {
11458
+ body = await res.json();
11459
+ } catch {
11460
+ throw new Error(`${what} failed: ${res.status} ${res.statusText} (response body was not JSON)`);
11461
+ }
11462
+ if (!res.ok) {
11463
+ const b = body ?? {};
11464
+ throw new Error(
11465
+ `${what} failed: ${b.message ?? res.statusText} (${b.code ?? res.status}${b.retryable ? ", retryable" : ""})`
11466
+ );
11467
+ }
11468
+ return body;
11469
+ }
11470
+ async function fundReceiverViaPaymaster(relayUrl, receiver, grantLamports) {
11471
+ assertAllowedRelayOrigin(relayUrl);
11472
+ if (grantLamports <= 0n || grantLamports > BigInt(Number.MAX_SAFE_INTEGER)) {
11473
+ throw new Error(
11474
+ `grantLamports must be a positive value representable as a JS number, got ${grantLamports}`
11475
+ );
11476
+ }
11477
+ const prepareRes = await relayFetch(`${relayUrl}/bridge/paymaster/prepare`, {
11478
+ method: "POST",
11479
+ headers: { "Content-Type": "application/json" },
11480
+ body: JSON.stringify({
11481
+ recipient: receiver.publicKey.toBase58(),
11482
+ grant_lamports: Number(grantLamports)
11483
+ })
11484
+ });
11485
+ const prepared = await parseBridgeResponse2(prepareRes, "paymaster prepare");
11486
+ if (Math.floor(Date.now() / 1e3) >= prepared.expires_at_unix) {
11487
+ throw new Error(
11488
+ "paymaster prepare returned a voucher that is already expired \u2014 refusing to sign a stale top-up rather than fail confusingly at cosign"
11489
+ );
11490
+ }
11491
+ const feeMint = new PublicKey12(prepared.fee_mint);
11492
+ const paymentAddress = new PublicKey12(prepared.payment_address);
11493
+ const feeTokenAmount = BigInt(prepared.fee_token_amount);
11494
+ const tx = Transaction3.from(Buffer.from(prepared.transaction, "base64"));
11495
+ validatePaymasterTopUpTransaction(tx, {
11496
+ recipient: receiver.publicKey,
11497
+ maxGrantLamports: grantLamports,
11498
+ feeMint,
11499
+ feeTokenAmount,
11500
+ paymentAddress
11501
+ });
11502
+ tx.partialSign(receiver);
11503
+ const cosignRes = await relayFetch(`${relayUrl}/bridge/paymaster/cosign`, {
11504
+ method: "POST",
11505
+ headers: { "Content-Type": "application/json" },
11506
+ body: JSON.stringify({
11507
+ transaction: tx.serialize({ requireAllSignatures: false, verifySignatures: false }).toString("base64"),
11508
+ voucher: prepared.voucher
11509
+ })
11510
+ });
11511
+ const cosigned = await parseBridgeResponse2(cosignRes, "paymaster cosign");
11512
+ return {
11513
+ transaction: Transaction3.from(Buffer.from(cosigned.transaction, "base64")),
11514
+ feeTokenAmount,
11515
+ feeMint: prepared.fee_mint,
11516
+ paymentAddress: prepared.payment_address
11517
+ };
11518
+ }
11519
+
11520
+ // src/bridge/derive.ts
11521
+ import { Keypair as Keypair5 } from "@solana/web3.js";
11522
+ import { hmac } from "@noble/hashes/hmac";
11523
+ import { sha512 } from "@noble/hashes/sha512";
11524
+ var BRIDGE_ESCROW_LABEL = "cloak_bridge_escrow";
11525
+ var MAX_RECEIVER_INDEX = 1e4;
11526
+ function deriveBridgeReceiver(nk, index) {
11527
+ if (!Number.isInteger(index) || index < 0) {
11528
+ throw new Error(`index must be a non-negative integer, got ${index}`);
11529
+ }
11530
+ if (index > MAX_RECEIVER_INDEX) {
11531
+ throw new Error(
11532
+ `index ${index} exceeds MAX_RECEIVER_INDEX (${MAX_RECEIVER_INDEX}). Indices must be a small sequential counter \u2014 discovery derives 0\u2026N and reads each on chain, so a timestamp index is unreachable by any scan and the deposit would be invisible to every device but this one.`
11533
+ );
11534
+ }
11535
+ const h = hmac(sha512, new Uint8Array(nk), new TextEncoder().encode(`${BRIDGE_ESCROW_LABEL}:${index}`));
11536
+ return Keypair5.fromSeed(h.slice(0, 32));
11537
+ }
11538
+
11539
+ // src/bridge/discover.ts
11540
+ import { SolanaJSONRPCError } from "@solana/web3.js";
11541
+ import { getAssociatedTokenAddressSync as getAssociatedTokenAddressSync4 } from "@solana/spl-token";
11542
+ function describeError(err) {
11543
+ return err instanceof Error ? err.message : String(err);
11544
+ }
11545
+ async function listBridgeDeposits(conn, nk, opts) {
11546
+ const scanDepth = Math.min(opts.scanDepth ?? 20, MAX_RECEIVER_INDEX + 1);
11547
+ const stopAfterUnused = opts.stopAfterUnused ?? 5;
11548
+ const out = [];
11549
+ let consecutiveUnused = 0;
11550
+ for (let index = 0; index < scanDepth; index++) {
11551
+ const kp = deriveBridgeReceiver(nk, index);
11552
+ const receiver = kp.publicKey;
11553
+ const tokenAccount = getAssociatedTokenAddressSync4(opts.mint, receiver, false);
11554
+ try {
11555
+ const [recvInfo, ataInfo] = await conn.getMultipleAccountsInfo([receiver, tokenAccount]);
11556
+ const lamports = recvInfo?.lamports ?? 0;
11557
+ let tokenBalance = 0n;
11558
+ if (ataInfo) {
11559
+ try {
11560
+ tokenBalance = BigInt((await conn.getTokenAccountBalance(tokenAccount)).value.amount);
11561
+ } catch (err) {
11562
+ if (!(err instanceof SolanaJSONRPCError && /could not find account/i.test(err.message))) {
11563
+ throw err;
11564
+ }
11565
+ }
11566
+ }
11567
+ if (lamports === 0 && !ataInfo) {
11568
+ consecutiveUnused++;
11569
+ if (consecutiveUnused >= stopAfterUnused) break;
11570
+ continue;
11571
+ }
11572
+ consecutiveUnused = 0;
11573
+ const shieldSignatures = [];
11574
+ const sigs = await conn.getSignaturesForAddress(receiver, { limit: 20 });
11575
+ for (const s of sigs) {
11576
+ if (s.err) continue;
11577
+ const tx = await conn.getTransaction(s.signature, { maxSupportedTransactionVersion: 0 });
11578
+ const keys = tx?.transaction.message.getAccountKeys({
11579
+ accountKeysFromLookups: tx.meta?.loadedAddresses
11580
+ });
11581
+ if (keys && [...Array(keys.length).keys()].some((i) => keys.get(i)?.equals(opts.programId))) {
11582
+ shieldSignatures.push(s.signature);
11583
+ }
11584
+ }
11585
+ const shieldSignature = shieldSignatures[0];
11586
+ const indexReused = shieldSignatures.length > 1;
11587
+ let state;
11588
+ if (tokenBalance > 0n) state = shieldSignature ? "needs-cleanup" : "arrived";
11589
+ else if (shieldSignature) state = ataInfo ? "needs-cleanup" : "complete";
11590
+ else state = "awaiting";
11591
+ out.push({
11592
+ index,
11593
+ receiver,
11594
+ tokenAccount,
11595
+ state,
11596
+ tokenBalance,
11597
+ lamports,
11598
+ shieldSignature,
11599
+ ...indexReused ? { indexReused, shieldSignatures } : {}
11600
+ });
11601
+ } catch (err) {
11602
+ out.push({
11603
+ index,
11604
+ receiver,
11605
+ tokenAccount,
11606
+ state: "unknown",
11607
+ tokenBalance: 0n,
11608
+ lamports: 0,
11609
+ error: describeError(err)
11610
+ });
11611
+ }
11612
+ }
11613
+ return out;
11614
+ }
11615
+
11616
+ // src/bridge/deposit-core.ts
11617
+ import {
11618
+ SystemProgram as SystemProgram4,
11619
+ Transaction as Transaction4,
11620
+ VersionedTransaction as VersionedTransaction2,
11621
+ sendAndConfirmTransaction as sendAndConfirmTransaction2
11622
+ } from "@solana/web3.js";
11623
+ import nacl9 from "tweetnacl";
11624
+
11625
+ // src/bridge/funder.ts
11626
+ import { PublicKey as PublicKey14 } from "@solana/web3.js";
11627
+ import { getAssociatedTokenAddressSync as getAssociatedTokenAddressSync5 } from "@solana/spl-token";
11628
+ var MAINNET_RENT_0 = 890880;
11629
+ var MAINNET_RENT_1 = 897840;
11630
+ var FEE_BUDGET = 13e4;
11631
+ var CLEANUP_FEE_BUDGET = 5e3;
11632
+ var pda = (seeds, programId) => PublicKey14.findProgramAddressSync(seeds.map((s) => Buffer.from(s)), programId)[0];
11633
+ function deriveFundingTargets(d) {
11634
+ const pool = pda([Buffer.from("pool"), d.mint.toBuffer()], d.programId);
11635
+ return {
11636
+ pool,
11637
+ // program: NullifierAccount::derive_pda_with_pool -> [b"nullifier", pool, nullifier]
11638
+ nullifier0: pda([Buffer.from("nullifier"), pool.toBuffer(), d.nullifiers[0]], d.programId),
11639
+ nullifier1: pda([Buffer.from("nullifier"), pool.toBuffer(), d.nullifiers[1]], d.programId),
11640
+ // relay: derive_risk_nonce_pda -> [b"risk_nonce", bind0]
11641
+ riskNonce: pda([Buffer.from("risk_nonce"), d.bind0], d.programId),
11642
+ depositorAta: getAssociatedTokenAddressSync5(d.mint, d.depositor, false)
11643
+ };
11644
+ }
11645
+ async function readRent(conn) {
11646
+ const [zero, one] = await Promise.all([
11647
+ conn.getMinimumBalanceForRentExemption(0),
11648
+ conn.getMinimumBalanceForRentExemption(1)
11649
+ ]);
11650
+ return { zero, one };
11651
+ }
11652
+
11653
+ // src/bridge/deposit-core.ts
11654
+ var DepositError = class extends Error {
11655
+ constructor(message, fundedAccounts, measuredTxSize) {
11656
+ super(message);
11657
+ this.fundedAccounts = fundedAccounts;
11658
+ this.measuredTxSize = measuredTxSize;
11659
+ this.name = "DepositError";
11660
+ }
11661
+ };
11662
+ async function depositFromDerivedKey(conn, R, funder, amount, log = console.log, grantOverride, opts) {
11663
+ if (!opts.noteSpendKey || opts.noteSpendKey.length !== 32) {
11664
+ throw new Error(
11665
+ "opts.noteSpendKey (32 bytes) is required. It derives the note's spending key, and a note whose key is not derived from something you can reproduce is unspendable forever. Pass the same Cloak seed you derived the receiving address from."
11666
+ );
11667
+ }
11668
+ if (!opts.relayUrl) {
11669
+ throw new Error(
11670
+ "opts.relayUrl is required. There is no default: a missing relay used to resolve silently to the local stack, which is how a live run built a deposit against infrastructure that only existed on someone's laptop. Say which relay, every call."
11671
+ );
11672
+ }
11673
+ if (!opts.programId) {
11674
+ throw new Error(
11675
+ "opts.programId is required. There is no default: this is the exact field a live mainnet run once inherited from this module's local-stack constant, built a deposit against a program that does not exist on mainnet, and failed hunting a merkle tree that was never there."
11676
+ );
11677
+ }
11678
+ if (!opts.mint) {
11679
+ throw new Error(
11680
+ "opts.mint is required. There is no default: a deposit built for the wrong mint produces a note for an asset that never arrived at R, and there is nothing to silently fall back to."
11681
+ );
11682
+ }
11683
+ assertAllowedRpcConnection(conn);
11684
+ const relayUrl = opts.relayUrl;
11685
+ const programId = opts.programId;
11686
+ const mint = opts.mint;
11687
+ setCircuitsPath(resolveCircuitsBase());
11688
+ const grantR = grantOverride ?? MAINNET_RENT_0 + FEE_BUDGET + CLEANUP_FEE_BUDGET;
11689
+ const heldByR = await conn.getBalance(R.publicKey);
11690
+ if (heldByR >= grantR) {
11691
+ log(` R already holds ${heldByR} (>= ${grantR}) \u2014 no top-up needed`);
11692
+ } else {
11693
+ await sendAndConfirmTransaction2(conn, new Transaction4().add(
11694
+ SystemProgram4.transfer({ fromPubkey: funder.publicKey, toPubkey: R.publicKey, lamports: grantR - heldByR })
11695
+ ), [funder]);
11696
+ log(` funder topped R up by ${grantR - heldByR} to ${grantR}${grantOverride !== void 0 ? " (OVERRIDDEN for a deliberate test)" : " \u2014 floor + deposit fee + cleanup fee, no rent"}`);
11697
+ }
11698
+ let measuredSize = -1;
11699
+ let fundedAccounts = [];
11700
+ const signTransaction2 = async (tx) => {
11701
+ if (tx instanceof VersionedTransaction2) {
11702
+ measuredSize = tx.serialize().length;
11703
+ const alts = await Promise.all(
11704
+ tx.message.addressTableLookups.map(async (l) => (await conn.getAddressLookupTable(l.accountKey)).value)
11705
+ );
11706
+ const keys = tx.message.getAccountKeys({ addressLookupTableAccounts: alts });
11707
+ const ix = tx.message.compiledInstructions.find((i) => keys.get(i.programIdIndex).equals(programId));
11708
+ const at = (n) => keys.get(ix.accountKeyIndexes[n]);
11709
+ const targets = [
11710
+ { a: at(12), need: MAINNET_RENT_0, name: "risk_nonce" },
11711
+ { a: at(4), need: MAINNET_RENT_1, name: "nullifier_0" },
11712
+ { a: at(5), need: MAINNET_RENT_1, name: "nullifier_1" }
11713
+ ];
11714
+ const infos = await conn.getMultipleAccountsInfo(targets.map((t) => t.a));
11715
+ fundedAccounts = targets.map((t, i) => ({
11716
+ name: t.name,
11717
+ address: t.a.toBase58(),
11718
+ lamportsSent: Math.max(0, t.need - (infos[i]?.lamports ?? 0))
11719
+ }));
11720
+ const ixs = targets.flatMap((t, i) => (infos[i]?.lamports ?? 0) >= t.need ? [] : [SystemProgram4.transfer({
11721
+ fromPubkey: funder.publicKey,
11722
+ toPubkey: t.a,
11723
+ lamports: t.need - (infos[i]?.lamports ?? 0)
11724
+ })]);
11725
+ if (ixs.length) {
11726
+ const sig = await sendAndConfirmTransaction2(conn, new Transaction4().add(...ixs), [funder]);
11727
+ log(` seam: funded ${ixs.length} program accounts read off the built tx \u2014 ${sig.slice(0, 16)}\u2026`);
11728
+ }
11729
+ tx.sign([R]);
11730
+ return tx;
11731
+ }
11732
+ tx.partialSign(R);
11733
+ return tx;
11734
+ };
11735
+ const signMessage = async (m) => nacl9.sign.detached(m, R.secretKey);
11736
+ const utxoKeypair = await deriveUtxoKeypairFromSpendKey(opts.noteSpendKey);
11737
+ const nk = getNkFromUtxoPrivateKey(utxoKeypair.privateKey);
11738
+ const { utxo, noteSalt } = await createRecoverableDepositUtxo(amount, nk, mint);
11739
+ const zeroInput = await createZeroUtxo(mint);
11740
+ const rBefore = await conn.getBalance(R.publicKey);
11741
+ let result;
11742
+ try {
11743
+ result = await transact(
11744
+ { inputUtxos: [zeroInput], outputUtxos: [utxo], externalAmount: amount, depositor: R.publicKey },
11745
+ {
11746
+ connection: conn,
11747
+ programId,
11748
+ relayUrl,
11749
+ // NO depositorKeypair: the SDK branches `if (keypair) … else if (signTransaction)`
11750
+ // (transact.ts:2786, :2936), so passing one silently skips the funding seam.
11751
+ depositorPublicKey: R.publicKey,
11752
+ walletPublicKey: R.publicKey,
11753
+ signTransaction: signTransaction2,
11754
+ signMessage,
11755
+ chainNoteViewingKeyNk: nk,
11756
+ chainNoteSalt: noteSalt,
11757
+ relaySupplementalAlt: true,
11758
+ onProgress: (s) => log(` \xB7 ${s}`)
11759
+ }
11760
+ );
11761
+ } catch (e) {
11762
+ const msg = e instanceof Error ? e.message : String(e);
11763
+ throw new DepositError(msg, fundedAccounts, measuredSize);
11764
+ }
11765
+ const rAfter = await conn.getBalance(R.publicKey);
11766
+ const noteIndex = result.outputUtxos[0].index;
11767
+ if (noteIndex === void 0) throw new Error("deposit landed but the note has no index \u2014 it cannot be discovered later");
11768
+ return {
11769
+ signature: result.signature,
11770
+ noteIndex,
11771
+ amount: result.outputUtxos[0].amount,
11772
+ txSize: measuredSize,
11773
+ rBefore,
11774
+ rAfter,
11775
+ rentExempt: rAfter >= MAINNET_RENT_0,
11776
+ inputNullifiers: result.inputNullifiers,
11777
+ fundedAccounts
11778
+ };
11779
+ }
11780
+
11781
+ // src/bridge/cleanup.ts
11782
+ import {
11783
+ PublicKey as PublicKey16,
11784
+ Transaction as Transaction5,
11785
+ sendAndConfirmTransaction as sendAndConfirmTransaction3
11786
+ } from "@solana/web3.js";
11787
+ import {
11788
+ getAssociatedTokenAddressSync as getAssociatedTokenAddressSync6,
11789
+ createCloseAccountInstruction,
11790
+ createTransferInstruction,
11791
+ createAssociatedTokenAccountIdempotentInstruction,
11792
+ getMinimumBalanceForRentExemptAccount
11793
+ } from "@solana/spl-token";
11794
+ var DEFAULT_MINT = new PublicKey16("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
11795
+ async function cleanupReceivingAddress(conn, R, dustDestination, mint = DEFAULT_MINT) {
11796
+ assertAllowedRpcConnection(conn);
11797
+ const rAta = getAssociatedTokenAddressSync6(mint, R.publicKey);
11798
+ const info = await conn.getAccountInfo(rAta);
11799
+ if (!info) {
11800
+ return {
11801
+ closed: false,
11802
+ dustSwept: 0n,
11803
+ rentReturned: 0,
11804
+ destination: null,
11805
+ signature: null,
11806
+ note: "no token account \u2014 nothing to recover"
11807
+ };
11808
+ }
11809
+ const bal = BigInt((await conn.getTokenAccountBalance(rAta)).value.amount);
11810
+ const rentHeld = info.lamports;
11811
+ const before = await conn.getBalance(R.publicKey);
11812
+ const ixs = [];
11813
+ let destination = null;
11814
+ if (bal > 0n && !dustDestination) {
11815
+ return {
11816
+ closed: false,
11817
+ dustSwept: 0n,
11818
+ rentReturned: 0,
11819
+ destination: null,
11820
+ signature: null,
11821
+ note: `dusted with ${bal} base units after the deposit; leaving the account open and ${rentHeld.toLocaleString()} lamports of rent unrecovered, because sweeping it would publish a link from this deposit to whoever receives it`
11822
+ };
11823
+ }
11824
+ if (bal > 0n && dustDestination) {
11825
+ const destAta = getAssociatedTokenAddressSync6(mint, dustDestination);
11826
+ const destInfo = await conn.getAccountInfo(destAta);
11827
+ if (!destInfo) {
11828
+ const ataRent = await getMinimumBalanceForRentExemptAccount(conn);
11829
+ if (before < ataRent + CLEANUP_FEE_BUDGET) {
11830
+ throw new Error(
11831
+ `dustDestination ${dustDestination.toBase58()} has no token account for mint ${mint.toBase58()}, and creating one costs ${ataRent} lamports of rent that R does not have: R holds ${before}, which deposit-core.ts reserved only for this transaction's own fee (CLEANUP_FEE_BUDGET = ${CLEANUP_FEE_BUDGET} in funder.ts), not for a fresh account's rent. Fund R with at least ${ataRent + CLEANUP_FEE_BUDGET - before} more lamports first, or choose a destination that already has a token account for this mint.`
11832
+ );
11833
+ }
11834
+ ixs.push(createAssociatedTokenAccountIdempotentInstruction(R.publicKey, destAta, dustDestination, mint));
11835
+ }
11836
+ ixs.push(createTransferInstruction(rAta, destAta, R.publicKey, bal));
11837
+ destination = destAta.toBase58();
11838
+ }
11839
+ ixs.push(createCloseAccountInstruction(rAta, R.publicKey, R.publicKey));
11840
+ const signature = await sendAndConfirmTransaction3(conn, new Transaction5().add(...ixs), [R], {
11841
+ commitment: "confirmed"
11842
+ });
11843
+ const after = await conn.getBalance(R.publicKey);
11844
+ const gone = await conn.getAccountInfo(rAta) === null;
11845
+ return {
11846
+ closed: gone,
11847
+ dustSwept: bal,
11848
+ rentReturned: after - before,
11849
+ destination,
11850
+ signature,
11851
+ note: bal > 0n ? `swept ${bal} base units of dust to the caller-chosen destination and closed, in one transaction \u2014 note this publishes a link from the deposit to that destination` : `closed cleanly, nothing to sweep`
11852
+ };
11853
+ }
11854
+
11855
+ // src/bridge/quote.ts
11856
+ var MIN_DEPOSIT_SPL_BASE_UNITS = 1000000n;
11857
+ var WITHDRAW_FIXED_FEE = 450000n;
11858
+ var WITHDRAW_FEE_BPS = 30n;
11859
+ function withdrawFeeAt(amount, fixedFee, feeBps) {
11860
+ return fixedFee + amount * feeBps / 10000n;
11861
+ }
11862
+ var MAX_FEE_FRACTION = 0.063;
11863
+ var FIXED_ROUND_TRIP = 903000n;
11864
+ var ECONOMIC_MINIMUM = FIXED_ROUND_TRIP * 1000000n / BigInt(Math.round((MAX_FEE_FRACTION - 3e-3) * 1e6));
11865
+ function withdrawFeeFor(amount) {
11866
+ return withdrawFeeAt(amount, WITHDRAW_FIXED_FEE, WITHDRAW_FEE_BPS);
11867
+ }
11868
+ function assessBridgeQuote(q) {
11869
+ if (q.sent < 0n) {
11870
+ throw new Error(`assessBridgeQuote: sent must not be negative (got ${q.sent}).`);
11871
+ }
11872
+ if (q.arrivesMin > q.sent) {
11873
+ throw new Error(
11874
+ `assessBridgeQuote: arrivesMin (${q.arrivesMin}) exceeds sent (${q.sent}) \u2014 no rail returns more than it was given, so this input did not come from a real quote.`
11875
+ );
11876
+ }
11877
+ const shieldedMinRaw = q.arrivesMin - q.paymasterFee;
11878
+ const shieldedTargetRaw = q.arrivesTarget - q.paymasterFee;
11879
+ const shieldedMin = shieldedMinRaw > 0n ? shieldedMinRaw : 0n;
11880
+ const shieldedTarget = shieldedTargetRaw > 0n ? shieldedTargetRaw : 0n;
11881
+ const viable = shieldedMinRaw >= MIN_DEPOSIT_SPL_BASE_UNITS;
11882
+ const economic = q.sent >= ECONOMIC_MINIMUM;
11883
+ const withdrawFee = shieldedMin > 0n ? withdrawFeeAt(shieldedMin, q.liveWithdrawFixedFee ?? WITHDRAW_FIXED_FEE, q.liveWithdrawFeeBps ?? WITHDRAW_FEE_BPS) : 0n;
11884
+ const roundTripCost = q.sent - (shieldedMin > withdrawFee ? shieldedMin - withdrawFee : 0n);
11885
+ const roundTripFraction = q.sent > 0n ? Number(roundTripCost) / Number(q.sent) : 0;
11886
+ const reasons = [];
11887
+ if (!viable) {
11888
+ const short = MIN_DEPOSIT_SPL_BASE_UNITS - shieldedMinRaw;
11889
+ reasons.push(
11890
+ (shieldedMinRaw <= 0n ? `The ${fmt(q.paymasterFee)} paymaster fee alone is ${shieldedMinRaw < 0n ? `${fmt(-shieldedMinRaw)} more than` : "all of"} the ${fmt(q.arrivesMin)} the rail guarantees, so nothing would be left to shield.` : `Only ${fmt(shieldedMinRaw)} would be left to shield after the ${fmt(q.paymasterFee)} paymaster fee.`) + ` The program's minimum deposit is ${fmt(MIN_DEPOSIT_SPL_BASE_UNITS)}. Send at least ${fmt(q.sent + short)} to clear it.`
11891
+ );
11892
+ }
11893
+ if (viable && !economic) {
11894
+ reasons.push(
11895
+ `This works, but ${fmt(roundTripCost)} of ${fmt(q.sent)} goes to fees (${(roundTripFraction * 100).toFixed(0)}%). The costs are almost all fixed, so bridging ${fmt(ECONOMIC_MINIMUM)} or more spreads them much further.`
11896
+ );
11897
+ }
11898
+ return {
11899
+ ...q,
11900
+ shieldedMin,
11901
+ shieldedTarget,
11902
+ viable,
11903
+ economic,
11904
+ withdrawFee,
11905
+ roundTripCost,
11906
+ roundTripFraction,
11907
+ reasons
11908
+ };
11909
+ }
11910
+ var fmt = (v) => `${(Number(v) / 1e6).toFixed(6)} USDC`;
11911
+ function renderAssessment(a) {
11912
+ const L = [];
11913
+ L.push(` you send ${fmt(a.sent)}`);
11914
+ L.push(` arrives at least ${fmt(a.arrivesMin)} (target ${fmt(a.arrivesTarget)}, not a promise)`);
11915
+ L.push(` paymaster fee ${fmt(a.paymasterFee)} buys the receiver its SOL, so your wallet stays off chain`);
11916
+ L.push(` SHIELDED ${fmt(a.shieldedMin)} at least \u2014 this is what you end up with`);
11917
+ L.push(` to withdraw later ${fmt(a.withdrawFee)} the program's fee, whenever you take it out`);
11918
+ for (const r of a.reasons) L.push(`
11919
+ ${a.viable ? "NOTE" : "REFUSED"}: ${r}`);
11920
+ return L.join("\n");
11921
+ }
11922
+
11923
+ // src/bridge/retry.ts
11924
+ var TERMINAL_FAILURE = /DepositTooSmall|minimum is 1\.000000|insufficient|holds only|below the program|kora wants|over the .* ceiling/i;
11925
+ function isTerminalFailure(message) {
11926
+ return TERMINAL_FAILURE.test(message);
11927
+ }
11928
+ function classifyPostFailure(attempted, after) {
11929
+ if (after === null) return "unknown";
11930
+ if (after === 0n) return "landed";
11931
+ if (after >= attempted) return "did-not-land";
11932
+ return "unknown";
11933
+ }
11934
+ function mayRetry(verdict, message) {
11935
+ return verdict === "did-not-land" && !isTerminalFailure(message);
11936
+ }
11937
+ function retryDelayMs(attempt) {
11938
+ return 15e3 * Math.max(0, attempt - 1);
11939
+ }
11940
+ var DoNotRetry = class extends Error {
11941
+ constructor(message) {
11942
+ super(message);
11943
+ this.name = "DoNotRetry";
11944
+ }
11945
+ };
11946
+ async function withShieldRetries(attempt, hooks = {}) {
11947
+ const attempts = hooks.attempts ?? 3;
11948
+ const sleep4 = hooks.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
11949
+ let last = "";
11950
+ for (let n = 1; n <= attempts; n++) {
11951
+ if (n > 1) {
11952
+ const delay = retryDelayMs(n);
11953
+ hooks.onRetry?.(n, delay, last);
11954
+ await sleep4(delay);
11955
+ }
11956
+ try {
11957
+ return await attempt(n);
11958
+ } catch (e) {
11959
+ if (e instanceof DoNotRetry) throw e;
11960
+ last = e instanceof Error ? e.message : String(e);
11961
+ if (isTerminalFailure(last) || n === attempts) throw e;
11962
+ }
11963
+ }
11964
+ throw new Error("unreachable");
11965
+ }
11966
+
10860
11967
  // src/index.ts
10861
- var VERSION = "0.2.1";
11968
+ var VERSION = "0.2.3";
10862
11969
  var SCANNER_SUPPORTS_TRANSACT_SWAP = true;
10863
11970
  export {
11971
+ BRIDGE_ESCROW_LABEL,
10864
11972
  BUILD_ALLOWS_LOCAL_ENDPOINTS,
10865
11973
  CHAIN_NOTE_SALT_BITS,
11974
+ CLEANUP_FEE_BUDGET,
10866
11975
  CLOAK_PRODUCTION_RELAY_URL,
10867
11976
  CLOAK_PROGRAM_ID,
10868
11977
  CloakError,
@@ -10870,25 +11979,38 @@ export {
10870
11979
  DEFAULT_TRANSACTION_CIRCUITS_URL,
10871
11980
  DELIVERY_MEMO_TAG,
10872
11981
  DELIVERY_REGISTRY_SEED,
11982
+ DepositError,
11983
+ DoNotRetry,
11984
+ ECONOMIC_MINIMUM,
10873
11985
  EXPECTED_CIRCUIT_HASHES,
11986
+ FEE_BUDGET,
10874
11987
  FIXED_FEE_LAMPORTS,
10875
11988
  InsecureRandomnessError,
10876
11989
  LAMPORTS_PER_SOL,
10877
11990
  LocalStorageAdapter,
11991
+ MAINNET_RENT_0,
11992
+ MAINNET_RENT_1,
11993
+ MAX_RECEIVER_INDEX,
10878
11994
  MERKLE_TREE_HEIGHT2 as MERKLE_TREE_HEIGHT,
10879
11995
  MIN_DEPOSIT_LAMPORTS,
11996
+ MIN_DEPOSIT_SPL_BASE_UNITS,
10880
11997
  MemoryStorageAdapter,
10881
11998
  MerkleTree,
10882
11999
  NATIVE_SOL_MINT,
12000
+ ONECLICK_PUBKEY_B58,
10883
12001
  RECIPIENT_DELIVERY_CIPHERTEXT_LEN,
10884
12002
  RECIPIENT_DELIVERY_EPHEMERAL_PK_LEN,
10885
12003
  RECIPIENT_DELIVERY_NONCE_LEN,
10886
12004
  RECIPIENT_DELIVERY_NOTE_BYTES,
10887
12005
  RECIPIENT_DELIVERY_PLAINTEXT_LEN,
10888
12006
  RECIPIENT_DELIVERY_TAG_LEN,
12007
+ RELAY_BATCH_AUTH_MAX_ITEMS,
10889
12008
  RELAY_ORIGIN_ALLOWLIST,
12009
+ REQUEST_AUTH_BATCH_DOMAIN,
12010
+ REQUEST_AUTH_BATCH_MAX_AGE_SECONDS,
10890
12011
  REQUEST_AUTH_MAX_AGE_SECONDS,
10891
12012
  REQUEST_AUTH_MAX_FUTURE_SKEW_SECONDS,
12013
+ RelayBatchAuthCoordinator,
10892
12014
  RelayInternalError,
10893
12015
  RelayService,
10894
12016
  RootNotFoundError,
@@ -10898,6 +12020,7 @@ export {
10898
12020
  SettlementVerificationError,
10899
12021
  ShieldPoolErrors,
10900
12022
  SimpleWallet,
12023
+ TERMINAL_FAILURE,
10901
12024
  TRANSACTION_CIRCUITS_VERSION,
10902
12025
  TRANSACT_AUTH_FIELDS,
10903
12026
  TRANSACT_SWAP_AUTH_FIELDS,
@@ -10907,8 +12030,12 @@ export {
10907
12030
  VARIABLE_FEE_NUMERATOR,
10908
12031
  VARIABLE_FEE_RATE,
10909
12032
  VERSION,
12033
+ WITHDRAW_FEE_BPS,
12034
+ WITHDRAW_FIXED_FEE,
10910
12035
  assertDirectSubmissionLanded,
12036
+ assertInputMints,
10911
12037
  assertTransactionCircuitIntegrity,
12038
+ assessBridgeQuote,
10912
12039
  bigintToBytes32,
10913
12040
  bigintToHex,
10914
12041
  buildMerkleTree,
@@ -10916,6 +12043,7 @@ export {
10916
12043
  buildMerkleTreeFromRelay,
10917
12044
  buildRecipientDeliveryNotes,
10918
12045
  buildRelayAuthPreimage,
12046
+ buildRelayBatchAuthMessage,
10919
12047
  buildTransactRequestBody,
10920
12048
  bytesToHex,
10921
12049
  calculateFee,
@@ -10923,12 +12051,16 @@ export {
10923
12051
  calculateRelayFee,
10924
12052
  canRebuildMerkleTreeFromChain,
10925
12053
  canonicalJson,
12054
+ canonicalPayloadString,
10926
12055
  chainNoteFromBase64,
10927
12056
  chainNoteToBase64,
12057
+ classifyPostFailure,
10928
12058
  classifyRelayError,
12059
+ cleanupReceivingAddress,
10929
12060
  cleanupStalePendingOperations,
10930
12061
  clearPendingDeposits,
10931
12062
  clearPendingWithdrawals,
12063
+ cloakBridgeRail,
10932
12064
  computeChainNoteHash,
10933
12065
  computeExtDataHash,
10934
12066
  computeMerkleRoot,
@@ -10945,15 +12077,19 @@ export {
10945
12077
  createLogger,
10946
12078
  createRecoverableChangeUtxo,
10947
12079
  createRecoverableDepositUtxo,
12080
+ createRelayBatchAuthCoordinator,
10948
12081
  createUtxo,
10949
12082
  createZeroUtxo,
10950
12083
  decryptCompactChainNote,
10951
12084
  decryptComplianceMetadataWithMasterKey,
10952
12085
  decryptTransactionMetadata,
12086
+ depositFromDerivedKey,
12087
+ deriveBridgeReceiver,
10953
12088
  deriveChangeNoteBlinding,
10954
12089
  deriveDepositNoteSecrets,
10955
12090
  deriveDiversifiedViewingKey,
10956
12091
  deriveDiversifier,
12092
+ deriveFundingTargets,
10957
12093
  deriveInputNullifierPdas,
10958
12094
  derivePublicKey,
10959
12095
  deriveSpendKey,
@@ -10992,6 +12128,7 @@ export {
10992
12128
  formatErrorForLogging,
10993
12129
  formatSol,
10994
12130
  fullWithdraw,
12131
+ fundReceiverViaPaymaster,
10995
12132
  generateCloakKeys,
10996
12133
  generateCommitmentAsync,
10997
12134
  generateMasterSeed,
@@ -11027,18 +12164,21 @@ export {
11027
12164
  isReactNative,
11028
12165
  isRootNotFoundError,
11029
12166
  isSubmissionOutcomeUnknownResponse,
12167
+ isTerminalFailure,
11030
12168
  isValidHex,
11031
12169
  isValidRpcUrl,
11032
12170
  isValidSolanaAddress,
11033
12171
  isWithdrawAmountSufficient,
11034
12172
  isWithdrawable,
11035
12173
  keypairToAdapter,
12174
+ listBridgeDeposits,
11036
12175
  loadPendingDeposits,
11037
12176
  loadPendingWithdrawals,
11038
12177
  loadVerifiedCircuitArtifacts,
11039
12178
  matchChangeNote,
11040
12179
  matchDepositNote,
11041
12180
  matchSwapRefundLeaf,
12181
+ mayRetry,
11042
12182
  openRecipientDeliveryNote,
11043
12183
  parseAmount,
11044
12184
  parseDeliveryCarrierMemo,
@@ -11061,11 +12201,15 @@ export {
11061
12201
  randomDepositNoteSalt,
11062
12202
  randomFieldElement,
11063
12203
  readMerkleTreeState,
12204
+ readRent,
11064
12205
  recipientDeliveryNoteToBase64,
11065
12206
  registerViewingKey,
12207
+ relayRequestDigestHex,
11066
12208
  removePendingDeposit,
11067
12209
  removePendingWithdrawal,
12210
+ renderAssessment,
11068
12211
  resolveCircuitsBase,
12212
+ retryDelayMs,
11069
12213
  savePendingDeposit,
11070
12214
  savePendingWithdrawal,
11071
12215
  scanNotesForWallet,
@@ -11086,6 +12230,7 @@ export {
11086
12230
  swapWithChange,
11087
12231
  toComplianceReport,
11088
12232
  transact,
12233
+ transactBatch,
11089
12234
  transfer,
11090
12235
  truncate,
11091
12236
  tryDecryptNote,
@@ -11098,13 +12243,17 @@ export {
11098
12243
  validateDepositParams,
11099
12244
  validateNote,
11100
12245
  validateOutputsSum,
12246
+ validatePaymasterTopUpTransaction,
11101
12247
  validateRoot,
11102
12248
  validateTransfers,
11103
12249
  validateWalletConnected,
11104
12250
  validateWithdrawableNote,
11105
12251
  verifyAllCircuits,
11106
12252
  verifyCircuitIntegrity,
12253
+ verifyQuoteSignature,
11107
12254
  verifyUtxos,
11108
12255
  waitForRoot,
11109
- withTiming
12256
+ withShieldRetries,
12257
+ withTiming,
12258
+ withdrawFeeFor
11110
12259
  };