@cloak.dev/sdk 0.1.8 → 0.2.0

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.d.ts CHANGED
@@ -1394,10 +1394,27 @@ declare function hexToBytes(hex: string): Uint8Array;
1394
1394
  */
1395
1395
  declare function bytesToHex(bytes: Uint8Array, prefix?: boolean): string;
1396
1396
  /**
1397
- * Generate random bytes using Web Crypto API
1397
+ * Thrown when no cryptographically secure randomness source is reachable.
1398
+ *
1399
+ * `randomBytes` backs note spend keys, blindings, salts and nonces. A predictable
1400
+ * value there is unrecoverable — it deanonymizes and can drain the note — so the
1401
+ * SDK fails closed instead of degrading to a non-cryptographic generator.
1402
+ */
1403
+ declare class InsecureRandomnessError extends CloakError {
1404
+ constructor(message: string, originalError?: Error);
1405
+ }
1406
+ /**
1407
+ * Generate cryptographically secure random bytes.
1408
+ *
1409
+ * Source order: `globalThis.crypto.getRandomValues` first (works in every runtime
1410
+ * this SDK supports, under both ESM and CJS), then `node:crypto.randomFillSync`.
1411
+ * There is deliberately **no** insecure fallback: if neither source is usable this
1412
+ * throws {@link InsecureRandomnessError} naming every source that was tried and why
1413
+ * it failed.
1398
1414
  *
1399
1415
  * @param length - Number of bytes to generate
1400
1416
  * @returns Random bytes
1417
+ * @throws {InsecureRandomnessError} If no cryptographically secure source is available
1401
1418
  */
1402
1419
  declare function randomBytes(length: number): Uint8Array;
1403
1420
  /**
@@ -1672,6 +1689,67 @@ declare class RelayInternalError extends Error {
1672
1689
  */
1673
1690
  cachedTreeFromChain?: MerkleTree | undefined);
1674
1691
  }
1692
+ /**
1693
+ * Raised when a submission's outcome could NOT be established as "landed" from chain state.
1694
+ *
1695
+ * This is the failure side of settlement verification (see `core/settlement.ts`). It exists
1696
+ * because the campaign's worst outcome was not a failed transaction — it was an AMBIGUOUS one
1697
+ * whose error text carried no signature (REL-A-10: seven POSTs, the transaction landed, the SDK
1698
+ * threw `RelayInternalError`, and the user had nothing to look up). Losing the signature is what
1699
+ * turns a landed transaction into a support ticket.
1700
+ *
1701
+ * `outcome` is deliberately unambiguous for the caller:
1702
+ *
1703
+ * "landed" the transaction DID land — the input nullifier PDAs exist — but the relay never
1704
+ * returned a usable response, so the SDK cannot hand back commitment indices.
1705
+ * DO NOT RETRY: a retry spends nothing and fails 0x1020. Rescan to recover notes.
1706
+ * "not-landed" provably nothing landed. The inputs are unspent; retrying is safe.
1707
+ * "unknown" the evidence is contradictory or unavailable. Check `signature` on an
1708
+ * independent RPC before doing anything else.
1709
+ * "failed" the transaction is on chain and failed during execution. Inputs are unspent.
1710
+ */
1711
+ declare class SettlementVerificationError extends Error {
1712
+ /** What the chain evidence supports. See the class doc — each value implies a different action. */
1713
+ readonly outcome: "landed" | "not-landed" | "unknown" | "failed";
1714
+ /**
1715
+ * The signature to look up, when one is known. `null` means no counterparty ever gave the SDK
1716
+ * one — which is itself part of the report, not something to paper over.
1717
+ */
1718
+ readonly signature: string | null;
1719
+ /** The verifier's own statement of what was and was not proven. */
1720
+ readonly reason: string;
1721
+ /** True when the input nullifier PDAs were observed on chain. */
1722
+ readonly nullifiersSpent: boolean;
1723
+ /** The underlying relay/RPC error, when the failure started as one. */
1724
+ readonly cause?: unknown | undefined;
1725
+ constructor(message: string,
1726
+ /** What the chain evidence supports. See the class doc — each value implies a different action. */
1727
+ outcome: "landed" | "not-landed" | "unknown" | "failed",
1728
+ /**
1729
+ * The signature to look up, when one is known. `null` means no counterparty ever gave the SDK
1730
+ * one — which is itself part of the report, not something to paper over.
1731
+ */
1732
+ signature: string | null,
1733
+ /** The verifier's own statement of what was and was not proven. */
1734
+ reason: string,
1735
+ /** True when the input nullifier PDAs were observed on chain. */
1736
+ nullifiersSpent?: boolean,
1737
+ /** The underlying relay/RPC error, when the failure started as one. */
1738
+ cause?: unknown | undefined);
1739
+ /** True when retrying this exact spend is safe (nothing was consumed on chain). */
1740
+ get safeToRetry(): boolean;
1741
+ }
1742
+ /**
1743
+ * Pull the signature out of a relay error body.
1744
+ *
1745
+ * The relay's `SubmissionOutcomeUnknown` response is a 503 whose JSON carries
1746
+ * `{"code":"submission_outcome_unknown","retryable":true,"signature":"<sig>"}` (relay
1747
+ * `src/error.rs`). The SDK used to funnel that body into a generic retry and then throw an error
1748
+ * built from a LATER attempt's message, dropping the one field the user needs.
1749
+ */
1750
+ declare function parseRelayErrorSignature(responseText: string): string | null;
1751
+ /** True when a relay error body is the relay's own "I do not know if this landed" report. */
1752
+ declare function isSubmissionOutcomeUnknownResponse(responseText: string): boolean;
1675
1753
  /**
1676
1754
  * Classify a relay error (response body text + HTTP status) into one of the
1677
1755
  * structured error types above, or fall back to RelayInternalError.
@@ -2052,6 +2130,141 @@ declare function verifyUtxos(utxos: Utxo[], connection: Connection, programId: P
2052
2130
  */
2053
2131
  declare function preflightNullifiers(utxos: Utxo[], connection: Connection, programId: PublicKey, commitment?: "processed" | "confirmed" | "finalized"): Promise<void>;
2054
2132
 
2133
+ /**
2134
+ * Settlement verification — decide from CHAIN STATE whether a submission landed.
2135
+ *
2136
+ * Why this module exists (adversarial campaign, tier 2):
2137
+ *
2138
+ * X-S-01B A hostile relay answered `/transact` with a well-formed success body carrying a
2139
+ * phantom signature and commitment indices [4242, 4243]. The SDK returned SUCCESS.
2140
+ * Real-RPC `getSignatureStatuses(searchTransactionHistory)` -> null, and BOTH input
2141
+ * nullifier PDAs read `exists=false`. Nothing had landed.
2142
+ * X-S-03a A hostile RPC swallowed `sendTransaction` and fabricated
2143
+ * `{err: null, confirmationStatus: "finalized"}`. `confirmTransaction` was satisfied and
2144
+ * the SDK returned a signature for a transaction that was never forwarded. (A control
2145
+ * deposit landed honestly through the same proxy, so account reads were NOT tampered
2146
+ * with — only the submission and its status were.)
2147
+ *
2148
+ * Both defects have the same shape: the SDK reported success on the strength of what its
2149
+ * counterparty SAID, never on the strength of what the chain SHOWS.
2150
+ *
2151
+ * The ground truth used here is the one the campaign itself used to prove nothing landed: the
2152
+ * input NULLIFIER PDAs. Their addresses are derived locally from the proof's own public inputs
2153
+ * (`["nullifier", pool, nullifier]`), and the program creates one per non-zero input nullifier on
2154
+ * every landed transact — including deposits, whose padding slots still emit real nullifiers
2155
+ * (transaction.circom: "the slot still emits a real nullifier ... and consumed on-chain";
2156
+ * shield-pool/src/instructions/transact.rs skips only all-zero nullifiers).
2157
+ *
2158
+ * Nullifier presence is strictly stronger evidence than a signature status:
2159
+ *
2160
+ * - it is ACCOUNT STATE, so it survives an RPC that has no signature history for the slot
2161
+ * (Surfpool forks, pruned nodes, a load-balanced endpoint that missed the write); and
2162
+ * - the nullifier and the output commitments are fields of the SAME proof's public inputs, so
2163
+ * the only transaction that can create these PDAs is one that also appended exactly our
2164
+ * output commitments. "Nullifiers exist" therefore means "our outputs are in the tree".
2165
+ *
2166
+ * The signature status is still checked, because it is the only signal that can prove a
2167
+ * DEFINITIVE FAILURE (`err != null`) and because a signature the RPC has never heard of is the
2168
+ * fingerprint of a phantom. It is corroboration, never the sole basis for success.
2169
+ *
2170
+ * LIMIT, stated plainly: a single RPC endpoint that lies about ACCOUNT READS as well cannot be
2171
+ * caught by any single-endpoint client, and this module does not pretend to. It closes the two
2172
+ * observed attacks — a lying relay, and an RPC that lies only about submission/status — and it
2173
+ * downgrades every unproven outcome to an explicit, signature-carrying "unknown" instead of
2174
+ * silently reporting success.
2175
+ */
2176
+
2177
+ /**
2178
+ * The RPC surface settlement verification needs, as a structural type rather than a hard
2179
+ * dependency on `Connection`. A real `web3.js` Connection satisfies it; so does a test stub, which
2180
+ * is what lets the phantom-relay / lying-RPC regressions be reproduced without a validator.
2181
+ */
2182
+ interface SettlementConnection {
2183
+ getSignatureStatuses(signatures: string[], config?: {
2184
+ searchTransactionHistory?: boolean;
2185
+ }): Promise<{
2186
+ value: Array<{
2187
+ err: unknown | null;
2188
+ confirmationStatus?: string | null;
2189
+ } | null>;
2190
+ }>;
2191
+ getMultipleAccountsInfo(publicKeys: PublicKey[], commitmentOrConfig?: any): Promise<Array<unknown | null>>;
2192
+ }
2193
+ /**
2194
+ * What the chain says about a submission.
2195
+ *
2196
+ * - `landed` the input nullifier PDAs exist: the proof was consumed, so its output
2197
+ * commitments are in the tree. This is the ONLY status that may be reported as
2198
+ * success.
2199
+ * - `failed` the signature is on chain and carries an execution error. Nothing was applied;
2200
+ * the inputs are still spendable.
2201
+ * - `not-landed` no nullifier PDA exists and the signature (if any) is unknown to the RPC even
2202
+ * with `searchTransactionHistory`. The phantom-signature fingerprint.
2203
+ * - `unknown` the evidence is contradictory or incomplete — most importantly the case where
2204
+ * a status claims confirmation while no nullifier PDA exists. NEVER report this
2205
+ * as success and NEVER silently retry it: the caller must surface the signature.
2206
+ */
2207
+ type SettlementStatus = "landed" | "failed" | "not-landed" | "unknown";
2208
+ interface SettlementVerdict {
2209
+ status: SettlementStatus;
2210
+ /** The signature that was checked, when one was supplied and syntactically usable. */
2211
+ signature: string | null;
2212
+ /** Human-readable statement of what was and was not proven. Safe to put in an error message. */
2213
+ reason: string;
2214
+ /** True when every checkable input nullifier PDA was present on chain. */
2215
+ nullifiersSpent: boolean;
2216
+ /** What `getSignatureStatuses` reported for `signature`. */
2217
+ signatureStatus: "finalized" | "confirmed" | "processed" | "absent" | "error" | "unchecked";
2218
+ }
2219
+ interface ConfirmSettlementParams {
2220
+ connection: SettlementConnection;
2221
+ programId: PublicKey;
2222
+ mint: PublicKey;
2223
+ /** The proof's public input nullifiers. All-zero entries are padding and are skipped. */
2224
+ inputNullifiers: bigint[];
2225
+ /** The signature the counterparty reported, if it reported one. */
2226
+ signature?: string | null;
2227
+ /** Total time to wait for evidence to appear before giving a verdict. */
2228
+ timeoutMs?: number;
2229
+ pollIntervalMs?: number;
2230
+ onProgress?: (status: string) => void;
2231
+ }
2232
+ declare function isPlausibleSignature(value: string | null | undefined): value is string;
2233
+ /**
2234
+ * Derive the nullifier PDA for each non-zero input nullifier. Zero entries are the program's own
2235
+ * padding sentinel (`transact.rs`: "Skip zero nullifiers (padding inputs)") and create no account.
2236
+ */
2237
+ declare function deriveInputNullifierPdas(programId: PublicKey, mint: PublicKey, inputNullifiers: bigint[]): PublicKey[];
2238
+ /**
2239
+ * Establish, from chain state, whether a submission landed.
2240
+ *
2241
+ * Polls until either side of the question is answered or `timeoutMs` elapses. Every RPC error is
2242
+ * absorbed into the verdict rather than thrown: "I could not check" is `unknown`, which the caller
2243
+ * must surface — it is never success.
2244
+ */
2245
+ declare function confirmTransactSettlement(params: ConfirmSettlementParams): Promise<SettlementVerdict>;
2246
+ /**
2247
+ * Gate a DIRECT (self-signed) submission on chain state — X-S-03a.
2248
+ *
2249
+ * `connection.confirmTransaction` is not proof of anything: the campaign's proxy swallowed
2250
+ * `sendTransaction` and answered the follow-up status poll with a fabricated
2251
+ * `{err: null, confirmationStatus: "finalized"}`, and the SDK handed the caller a signature for a
2252
+ * transaction that was never forwarded. A control deposit landed honestly through the same proxy,
2253
+ * so this was not broken plumbing — it was the client believing a status field.
2254
+ *
2255
+ * Throws `SettlementVerificationError` unless the input nullifier PDAs prove the transaction was
2256
+ * applied. The signature is always carried on the error so the caller can look it up.
2257
+ */
2258
+ declare function assertDirectSubmissionLanded(params: {
2259
+ connection: SettlementConnection;
2260
+ programId: PublicKey;
2261
+ mint: PublicKey;
2262
+ nullifiers: Array<Uint8Array | bigint>;
2263
+ signature: string;
2264
+ timeoutMs?: number;
2265
+ onProgress?: (status: string) => void;
2266
+ }): Promise<SettlementVerdict>;
2267
+
2055
2268
  /**
2056
2269
  * Structured Logger for Cloak SDK
2057
2270
  *
@@ -2510,6 +2723,111 @@ declare function getNullifierPDA(poolPubkey: PublicKey, nullifier: Uint8Array |
2510
2723
  * @returns [PublicKey, bump] - The swap state PDA and its bump seed
2511
2724
  */
2512
2725
  declare function getSwapStatePDA(poolPubkey: PublicKey, nullifier: Uint8Array | Buffer, programId?: PublicKey): [PublicKey, number];
2726
+ /**
2727
+ * H-04: derive the per-pool RefundLedger PDA (conservation counter).
2728
+ *
2729
+ * Seeds: ["refund_ledger", pool_mint]
2730
+ */
2731
+ declare function getRefundLedgerPDA(mint?: PublicKey, programId?: PublicKey): [PublicKey, number];
2732
+ /**
2733
+ * H-04: derive the one-time RefundClaim PDA (replay guard for a voucher).
2734
+ *
2735
+ * Seeds: ["refund_claim", claim_id]
2736
+ */
2737
+ declare function getRefundClaimPDA(claimId: Uint8Array | Buffer, programId?: PublicKey): [PublicKey, number];
2738
+ /**
2739
+ * M-07/H-04: derive the per-pool PoolAuthorityConfig PDA (holds the
2740
+ * withdraw-authorizer the refund voucher must be signed by).
2741
+ *
2742
+ * Seeds: ["pool_authority", pool_mint]
2743
+ */
2744
+ declare function getPoolAuthorityConfigPDA(mint?: PublicKey, programId?: PublicKey): [PublicKey, number];
2745
+ /**
2746
+ * Registry PDA the relay's recipient-delivery carrier (CLKD1) touches so the carrier transaction is
2747
+ * enumerable via `getSignaturesForAddress`.
2748
+ *
2749
+ * Seed: ["cloak_delivery_registry"] — `DELIVERY_REGISTRY_SEED` in
2750
+ * `services/relay/src/solana/mod.rs::emit_recipient_delivery_carrier`. Mint-independent, exactly
2751
+ * like the chain-note registry: one registry per program, not per pool.
2752
+ */
2753
+ declare function getDeliveryRegistryPDA(programId?: PublicKey): PublicKey;
2754
+ /**
2755
+ * Registry PDA the relay's compliance chain-note carrier (CLK1) touches.
2756
+ *
2757
+ * Seed: ["cloak_chain_note_registry"] — `CHAIN_NOTE_REGISTRY_SEED` in
2758
+ * `services/relay/src/solana/mod.rs::emit_chain_note_carrier`.
2759
+ */
2760
+ declare function getChainNoteRegistryPDA(programId?: PublicKey): PublicKey;
2761
+
2762
+ /**
2763
+ * H-04 — claim-based timeout refund (SDK).
2764
+ *
2765
+ * When a private swap times out, `CloseSwapState` returns the parked principal
2766
+ * to the pool and credits the on-chain `RefundLedger`. The user later reclaims
2767
+ * that principal as a FRESH shielded note via `ClaimRefund`, gated by a
2768
+ * withdraw-authorizer voucher (signed by the relay) and a deposit-shape Groth16
2769
+ * proof — with no on-chain link back to the original swap.
2770
+ *
2771
+ * This module:
2772
+ * 1. asks the relay to sign the 81-byte refund voucher for `claim_id`,
2773
+ * 2. generates a deposit-shape proof (publicAmount = +refund_amount, zero
2774
+ * inputs, one fresh output note `C_r`) bound to the live merkle root,
2775
+ * 3. builds and submits the `[ed25519 voucher][CU][ClaimRefund]` transaction,
2776
+ * 4. returns the fresh refund UTXO for the caller to store/spend later.
2777
+ *
2778
+ * The proof construction is byte-for-byte the same as the standalone
2779
+ * `audit-tests/h-04/gen-claim-proof.cjs` generator that the program-side soak
2780
+ * verified, so the proof verifies against the deployed transaction vkey.
2781
+ */
2782
+
2783
+ /** A relay-signed refund voucher, as returned by `POST /refund-voucher`. */
2784
+ interface RefundVoucher {
2785
+ /** base64 Ed25519 signature over the 81-byte message. */
2786
+ signature: string;
2787
+ /** hex 81-byte voucher message. */
2788
+ message: string;
2789
+ /** base58 signer (pool withdraw-authorizer). */
2790
+ signer_pubkey: string;
2791
+ /** expiry slot bound into the voucher. */
2792
+ expiry_slot?: number;
2793
+ }
2794
+ interface ClaimTimeoutRefundParams {
2795
+ /** Refund principal in lamports (must equal the closed swap's parked amount). */
2796
+ refundAmount: bigint;
2797
+ /** Pool mint; ClaimRefund is SOL-pool only (defaults to WSOL sentinel). */
2798
+ poolMint?: PublicKey;
2799
+ /** 32-byte claim id = H(user secret). Random if omitted. */
2800
+ claimId?: Uint8Array;
2801
+ /** Supply a pre-fetched voucher to skip the relay round-trip. */
2802
+ voucher?: RefundVoucher;
2803
+ }
2804
+ interface ClaimTimeoutRefundOptions {
2805
+ connection: Connection;
2806
+ programId: PublicKey;
2807
+ /** Relay base URL (used to fetch the voucher when not supplied). */
2808
+ relayUrl?: string;
2809
+ /** Pays fees + signs the claim tx (relay-submitted is a documented follow-up). */
2810
+ payerKeypair: Keypair;
2811
+ onProgress?: (message: string) => void;
2812
+ }
2813
+ interface ClaimTimeoutRefundResult {
2814
+ /** Submitted ClaimRefund transaction signature. */
2815
+ signature: string;
2816
+ /** The fresh refund note created by the claim — store this to spend later. */
2817
+ refundUtxo: Utxo;
2818
+ /** The claim id consumed (hex). */
2819
+ claimId: string;
2820
+ /** The merkle leaf index where the refund note `C_r` landed. */
2821
+ leafIndex: number;
2822
+ }
2823
+ /**
2824
+ * Reclaim a timed-out swap's principal as a fresh shielded note via ClaimRefund.
2825
+ *
2826
+ * Front-running note: this self-submits the claim. The production default is
2827
+ * relay submission (decision #3) to avoid mempool exposure of the voucher; a
2828
+ * relay `/claim-refund` endpoint is the documented follow-up.
2829
+ */
2830
+ declare function claimTimeoutRefund(params: ClaimTimeoutRefundParams, options: ClaimTimeoutRefundOptions): Promise<ClaimTimeoutRefundResult>;
2513
2831
 
2514
2832
  /**
2515
2833
  * On-chain Merkle proof computation
@@ -2625,7 +2943,12 @@ declare function buildMerkleTreeFromRelay(relayUrl: string, options?: {
2625
2943
  maxRetries?: number;
2626
2944
  waitForIndex?: number;
2627
2945
  }): Promise<MerkleTree>;
2628
- declare function buildMerkleTreeFromChain(connection: Connection, programId: PublicKey, merkleTree: PublicKey, onProgress?: (message: string) => void): Promise<MerkleTree>;
2946
+ declare function buildMerkleTreeFromChain(connection: Connection, programId: PublicKey, merkleTree: PublicKey, onProgress?: (message: string) => void,
2947
+ /**
2948
+ * The pool mint this tree belongs to. Required in practice: without it a `CloseSwapState` refund
2949
+ * leaf cannot be attributed to a tree, and reconstruction fails closed rather than guess.
2950
+ */
2951
+ mint?: PublicKey): Promise<MerkleTree>;
2629
2952
  /**
2630
2953
  * Pre-flight root validation
2631
2954
  *
@@ -2679,288 +3002,186 @@ declare function preflightCheck(relayUrl: string, rootHex: string): Promise<{
2679
3002
  }>;
2680
3003
 
2681
3004
  /**
2682
- * Direct Circom WASM Proof Generation
3005
+ * The SDK's host-environment predicates, in one place (X-S-02C).
2683
3006
  *
2684
- * This module provides direct proof generation using snarkjs and Circom WASM,
2685
- * matching the approach used in services-new/tests/src/proof.ts
3007
+ * ── The defect this closes ────────────────────────────────────────────────────────────────────
3008
+ * The merkle-from-chain rebuild was guarded by TWO independently written predicates that disagreed
3009
+ * about React Native:
2686
3010
  *
2687
- * This uses pinned S3-hosted circuit artifacts and does not require
2688
- * a backend prover service.
2689
- */
2690
-
2691
- /**
2692
- * Default URL for fetching circuit artifacts
2693
- * Hosted in S3 and versioned by build.
2694
- */
2695
- declare const DEFAULT_CIRCUITS_URL = "https://storage.googleapis.com/cloak-circuits/circuits/0.1.0";
2696
- interface WithdrawRegularInputs {
2697
- root: bigint;
2698
- nullifier: bigint;
2699
- outputs_hash: bigint;
2700
- public_amount: bigint;
2701
- amount: bigint;
2702
- leaf_index: bigint;
2703
- sk: [bigint, bigint];
2704
- r: [bigint, bigint];
2705
- pathElements: bigint[];
2706
- pathIndices: number[];
2707
- num_outputs: number;
2708
- out_addr: bigint[][];
2709
- out_amount: bigint[];
2710
- out_flags: number[];
2711
- var_fee: bigint;
2712
- rem: bigint;
2713
- }
2714
- interface WithdrawSwapInputs {
2715
- sk_spend: bigint;
2716
- r: bigint;
2717
- amount: bigint;
2718
- leaf_index: bigint;
2719
- path_elements: bigint[];
2720
- path_indices: number[];
2721
- root: bigint;
2722
- nullifier: bigint;
2723
- outputs_hash: bigint;
2724
- public_amount: bigint;
2725
- input_mint: bigint[];
2726
- output_mint: bigint[];
2727
- recipient_ata: bigint[];
2728
- min_output_amount: bigint;
2729
- var_fee: bigint;
2730
- rem: bigint;
2731
- }
2732
- interface ProofResult {
2733
- proof: Groth16Proof;
2734
- publicSignals: string[];
2735
- proofBytes: Uint8Array;
2736
- publicInputsBytes: Uint8Array;
2737
- }
2738
- /**
2739
- * Generate Groth16 proof for regular withdrawal using Circom WASM
3011
+ * - the GATE in `core/transact.ts` `!IS_BROWSER && isMerkleClass && relayUrl`, where
3012
+ * `IS_BROWSER` was `typeof window !== "undefined" || typeof globalThis.document !== "undefined"`,
3013
+ * with NO React-Native carve-out. React Native defines `window`, so RN read as a browser and the
3014
+ * last-resort chain replay was silently skipped there.
3015
+ * - the BUILDER it guards — `utils/relay-client.ts::buildMerkleTreeFromChain`, whose own
3016
+ * `isBrowser()` short-circuits on `navigator.product === "ReactNative"` and therefore explicitly
3017
+ * PERMITS React Native to rebuild from chain.
2740
3018
  *
2741
- * This matches the approach in services-new/tests/src/proof.ts
3019
+ * So one half of the same mechanism classified RN as a browser and the other half classified it as
3020
+ * not-a-browser.
2742
3021
  *
2743
- * @param inputs - Circuit inputs
2744
- * @param circuitsPath - Ignored. Proof generation always uses pinned S3 circuits.
2745
- */
2746
- declare function generateWithdrawRegularProof(inputs: WithdrawRegularInputs, circuitsPath: string): Promise<ProofResult>;
2747
- /**
2748
- * Generate Groth16 proof for swap withdrawal using Circom WASM
3022
+ * ── The decision: the BUILDER is right, and the gate was wrong ────────────────────────────────
3023
+ * The browser fails closed for a stated reason a full signature-history scan is too slow and too
3024
+ * unreliable on a browser main thread, and a browser always has a reachable relay whose tree it can
3025
+ * use instead. Neither half of that reasoning holds for React Native: it is not a browser, it has no
3026
+ * DOM, it is not running on a page's main thread, and on a local node it has no relay-tree fallback
3027
+ * at all. Closing the gate against RN removed its ONLY recovery path from a drifted relay tree —
3028
+ * precisely the class of failure the fallback exists for — while leaving the builder it calls happy
3029
+ * to serve it.
2749
3030
  *
2750
- * This matches the approach in services-new/tests/src/proof.ts
3031
+ * The alternative reading (make the builder refuse RN too, so the two agree by failing closed
3032
+ * everywhere) was rejected: it agrees by breaking the Cloak mobile wallet, whose merkle path depends
3033
+ * on the carve-out, and it discards a documented, deliberate decision in favour of an undocumented
3034
+ * accident. The gate's own comment never mentions React Native — it argues about SSR — which is what
3035
+ * an accident looks like.
2751
3036
  *
2752
- * @param inputs - Circuit inputs
2753
- * @param circuitsPath - Ignored. Proof generation always uses pinned S3 circuits.
3037
+ * ── Two predicates, deliberately, and the difference is not RN ────────────────────────────────
3038
+ * `isBrowser()` is a HARD REFUSAL "this host cannot do it at all" — so it demands a real DOM
3039
+ * (`window` AND `document`).
3040
+ * `isBrowserLike()` is a CONSERVATIVE OPT-OUT — "don't start something slow here" — so `window` OR
3041
+ * `document` is enough, which keeps an SSR host that polyfills only `document` from being mistaken
3042
+ * for Node and made to attempt a chain replay.
3043
+ *
3044
+ * They differ on SSR on purpose. They must NEVER differ on React Native again, which is why both are
3045
+ * built from the single `isReactNative()` below.
3046
+ *
3047
+ * All three are functions, not module-scope constants: a constant is frozen at import time, so any
3048
+ * host that installs its globals after the bundle loads — and any test that wants to pin the three
3049
+ * environments — reads a stale answer.
2754
3050
  */
2755
- declare function generateWithdrawSwapProof(inputs: WithdrawSwapInputs, circuitsPath: string): Promise<ProofResult>;
2756
3051
  /**
2757
- * Check if circuits are available from the pinned S3 source.
3052
+ * True on React Native. `navigator.product === "ReactNative"` is the canonical flag and is what
3053
+ * `utils/proof-generation.ts` has always used.
2758
3054
  */
2759
- declare function areCircuitsAvailable(circuitsPath: string): Promise<boolean>;
3055
+ declare function isReactNative(): boolean;
2760
3056
  /**
2761
- * Get default circuits URL.
3057
+ * True only on a real browser: a DOM host with both `window` and `document`, and not React Native.
3058
+ *
3059
+ * Use for HARD refusals — work this host genuinely cannot perform.
2762
3060
  */
2763
- declare function getDefaultCircuitsPath(): Promise<string>;
3061
+ declare function isBrowser(): boolean;
2764
3062
  /**
2765
- * Pinned circuit artifact hashes (SHA-256).
3063
+ * True on a browser OR on a DOM-ish host such as SSR that defines only one of `window`/`document`,
3064
+ * and false on React Native and on Node.
2766
3065
  *
2767
- * These hashes must be updated whenever the trusted circuit artifacts change.
3066
+ * Use for CONSERVATIVE opt-outs expensive work that should not be started speculatively.
2768
3067
  */
2769
- declare const EXPECTED_CIRCUIT_HASHES: {
2770
- withdraw_regular_wasm: string;
2771
- withdraw_regular_zkey: string;
2772
- withdraw_swap_wasm: string;
2773
- withdraw_swap_zkey: string;
2774
- };
3068
+ declare function isBrowserLike(): boolean;
3069
+
2775
3070
  /**
2776
- * Circuit verification result
2777
- */
2778
- interface CircuitVerificationResult {
2779
- /** Whether verification passed */
2780
- valid: boolean;
2781
- /** Which circuit was checked */
2782
- circuit: 'withdraw_regular' | 'withdraw_swap';
2783
- /** Error message if verification failed */
2784
- error?: string;
2785
- computed?: {
2786
- wasm: string;
2787
- zkey: string;
2788
- };
2789
- expected?: {
2790
- wasm: string;
2791
- zkey: string;
2792
- };
2793
- }
3071
+ * Circuit artifact releases — the single source of truth for
3072
+ * "which bundle version" and "which bytes are that bundle".
3073
+ *
3074
+ * Why this file exists
3075
+ * -------------------
3076
+ * The version segment of the artifact base URL and the pinned SHA-256 digests
3077
+ * used to be written out by hand in two unrelated places
3078
+ * (`utils/proof-generation.ts` held `.../circuits/0.1.0` plus a digest table,
3079
+ * `core/transact.ts` held its own copy of the same URL literal). They drifted:
3080
+ * the default base pointed at the `0.1.0` bundle while the pinned `transaction`
3081
+ * digests were the `0.2.0` trusted-setup ceremony output, so every default-config
3082
+ * proof died on a digest mismatch several megabytes into proof generation.
3083
+ *
3084
+ * Here a bundle is declared once, as a version plus the digests of the artifacts
3085
+ * that version contains, and the base URL is *derived* from that version by
3086
+ * {@link defineCircuitBundle}. There is no way to write a URL whose version
3087
+ * segment disagrees with the digests next to it:
3088
+ *
3089
+ * - **compile time** — `baseUrl` is typed `` `${string}/circuits/${V}` ``, where
3090
+ * `V` is the bundle's own `version` literal, and {@link BUNDLE_BY_CIRCUIT} is
3091
+ * an exhaustive `Record<CircuitName, …>`, so a new circuit with no bundle, or
3092
+ * an assertion about a version the bundle does not carry, fails `tsc`.
3093
+ * - **startup** — {@link assertCircuitReleaseConsistency} runs at module load
3094
+ * and throws if a bundle's digests are malformed, if a bundle does not pin the
3095
+ * circuit that maps to it, or if a hand-edited base URL stops ending in its own
3096
+ * version. That is an import-time failure, not a failure deep inside
3097
+ * `groth16.fullProve`.
3098
+ *
3099
+ * Publication is deliberately *not* assumed. A bundle whose bytes this SDK has
3100
+ * not verified at a public location carries `baseUrl: null`; callers must point
3101
+ * the SDK at a location themselves (`setCircuitsPath`). Nothing here silently
3102
+ * defaults to a URL that has not been checked against the digests below.
3103
+ */
3104
+ /** Circuits whose artifacts carry a pinned SHA-256 digest in this SDK. */
3105
+ type CircuitName = 'withdraw_regular' | 'withdraw_swap' | 'transaction';
3106
+ /** Version of the trusted-setup ceremony bundle that froze the `transaction` circuit. */
3107
+ declare const TRANSACTION_CIRCUITS_VERSION = "0.2.0";
3108
+
2794
3109
  /**
2795
- * Verify circuit integrity by checking verification key hashes
3110
+ * UTXO Transaction Methods
2796
3111
  *
2797
- * This function fetches the verification key and computes its hash,
2798
- * then compares against the expected hash embedded in the SDK.
3112
+ * This module provides high-level methods for UTXO transactions:
3113
+ * - transact(): Core UTXO transaction
3114
+ * - transfer(): Shield-to-shield transfer
3115
+ * - partialWithdraw(): Withdraw with change
3116
+ */
3117
+
3118
+ /**
3119
+ * Can this host attempt the last-resort "rebuild the merkle tree from chain" recovery?
2799
3120
  *
2800
- * IMPORTANT: If the hashes don't match, the circuit may produce proofs
2801
- * that will be rejected by the on-chain verifier!
3121
+ * X-S-02C: this gate and the builder it guards (`buildMerkleTreeFromChain`, which refuses only on a
3122
+ * real browser and EXPLICITLY permits React Native) used to be written separately and disagreed
3123
+ * RN defines `window`, so the old inline `typeof window !== "undefined" || typeof document !==
3124
+ * "undefined"` classified RN as a browser and skipped the rebuild, removing RN's only recovery path
3125
+ * from a drifted relay tree while the builder was perfectly willing to serve it. Both now come from
3126
+ * `utils/environment`, so they cannot disagree about React Native again.
2802
3127
  *
2803
- * @param circuitsPath - Ignored. Verification always uses pinned S3 circuits.
2804
- * @param circuit - Which circuit to verify ('withdraw_regular' or 'withdraw_swap')
2805
- * @returns Verification result
3128
+ * The SSR conservatism is kept: `isBrowserLike()` is true when EITHER `window` or `document` exists,
3129
+ * because an SSR host that polyfills only `document` must not be mistaken for Node and made to run a
3130
+ * full signature-history scan.
2806
3131
  *
2807
- * @example
2808
- * ```typescript
2809
- * const result = await verifyCircuitIntegrity('https://storage.googleapis.com/cloak-circuits/circuits/0.1.0', 'withdraw_regular');
2810
- * if (!result.valid) {
2811
- * console.error('Circuit verification failed:', result.error);
2812
- * // Don't proceed with proof generation!
2813
- * }
2814
- * ```
3132
+ * A function, not a module-scope constant: the constant was frozen at import time, so a host that
3133
+ * installs its globals after the bundle loads read a stale answer.
2815
3134
  */
2816
- declare function verifyCircuitIntegrity(circuitsPath: string, circuit: 'withdraw_regular' | 'withdraw_swap'): Promise<CircuitVerificationResult>;
3135
+ declare function canRebuildMerkleTreeFromChain(): boolean;
3136
+
2817
3137
  /**
2818
- * Verify all circuits before use
2819
- *
2820
- * Call this at SDK initialization to ensure circuits are valid.
3138
+ * Base the ceremony-frozen `transaction` artifacts are fetched from by default.
2821
3139
  *
2822
- * @param circuitsPath - Ignored. Verification always uses pinned S3 circuits.
2823
- * @returns Array of verification results (one per circuit)
3140
+ * Derived from {@link TRANSACTION_CIRCUIT_BUNDLE} the same record that pins the
3141
+ * digests so the version in the URL and the digests checked against it cannot
3142
+ * drift apart. It is `null` while this SDK build pins no location whose bytes
3143
+ * were verified to hash to those digests; that makes an unconfigured SDK a
3144
+ * `tsc` error at the call site (`setCircuitsPath(DEFAULT_TRANSACTION_CIRCUITS_URL)`
3145
+ * does not type-check against `string`) and an immediate, explanatory throw at
3146
+ * runtime, instead of a digest mismatch ~22 MB into proof generation.
2824
3147
  */
2825
- declare function verifyAllCircuits(circuitsPath: string): Promise<CircuitVerificationResult[]>;
3148
+ declare const DEFAULT_TRANSACTION_CIRCUITS_URL: string | null;
2826
3149
 
2827
3150
  /**
2828
- * Pending Operations Manager
2829
- *
2830
- * Utility for persisting pending deposit/withdrawal operations in browser storage.
2831
- * This enables recovery if the browser crashes or user navigates away mid-operation.
3151
+ * Set circuits base path: local directory containing `transaction_js/` and `transaction_final.zkey`,
3152
+ * or an `http(s)` base URL to those artifacts (loaded into memory once per process).
2832
3153
  *
2833
- * IMPORTANT: This uses localStorage by default which has security implications.
2834
- * Notes contain sensitive spending keys - consider using more secure storage
2835
- * in production (e.g., encrypted IndexedDB, secure enclave).
3154
+ * No cache invalidation is needed here: verified artifact buffers are memoised
3155
+ * per base inside `proof-generation.ts`, so a new base loads and re-verifies its
3156
+ * own bytes.
2836
3157
  */
2837
-
3158
+ declare function setCircuitsPath(next: string): void;
2838
3159
  /**
2839
- * Pending deposit record
3160
+ * Get the current circuits path, or `null` when none is configured and this SDK
3161
+ * build pins no verified default (see `DEFAULT_TRANSACTION_CIRCUITS_URL`).
2840
3162
  */
2841
- interface PendingDeposit {
2842
- /** The note (contains spending secrets!) */
2843
- note: CloakNote;
2844
- /** When the deposit was initiated */
2845
- startedAt: number;
2846
- /** Transaction signature if available */
2847
- txSignature?: string;
2848
- /** Status of the deposit */
2849
- status: "pending" | "tx_sent" | "confirmed" | "failed";
2850
- /** Error message if failed */
2851
- error?: string;
2852
- }
3163
+ declare function getCircuitsPath(): string | null;
2853
3164
  /**
2854
- * Pending withdrawal record
3165
+ * Resolve a base to prove from: an explicit argument, else `CLOAK_CIRCUITS_PATH`
3166
+ * / `CLOAK_CIRCUITS` from the environment, else this build's pinned default.
3167
+ *
3168
+ * Use this instead of writing an artifact URL out by hand — a hand-written URL
3169
+ * is exactly how the base's version segment came to disagree with the digests
3170
+ * this SDK checks against. Throws an explanatory error (naming the expected
3171
+ * bundle version and both expected digests) when nothing resolves.
2855
3172
  */
2856
- interface PendingWithdrawal {
2857
- /** The relay request ID (for resumption) */
2858
- requestId: string;
2859
- /** The note commitment being withdrawn */
2860
- commitment: string;
2861
- /** The nullifier being used */
2862
- nullifier: string;
2863
- /** When the withdrawal was initiated */
2864
- startedAt: number;
2865
- /** Status of the withdrawal */
2866
- status: "pending" | "processing" | "completed" | "failed";
2867
- /** Transaction signature if completed */
2868
- txSignature?: string;
2869
- /** Error message if failed */
2870
- error?: string;
2871
- }
3173
+ declare function resolveCircuitsBase(explicit?: string): string;
2872
3174
  /**
2873
- * Save a pending deposit
2874
- * Call this BEFORE sending the on-chain transaction to ensure note is persisted
3175
+ * Chain note v4.
3176
+ *
3177
+ * v4 binds noteSemantics = Poseidon(outAmount[0], outPubkey[0], noteIsSendToSelfKey0) into the note
3178
+ * tail, matching the ceremony circuit. The tail is therefore a 4-input hash, not 3.
3179
+ *
3180
+ * noteIsSendToSelfKey0 is 1 only when publicAmount == 0 AND output 0 went to the spender's own key,
3181
+ * exactly as the circuit computes it.
2875
3182
  */
2876
- declare function savePendingDeposit(deposit: PendingDeposit): void;
2877
- /**
2878
- * Load all pending deposits
2879
- */
2880
- declare function loadPendingDeposits(): PendingDeposit[];
2881
- /**
2882
- * Update a pending deposit status
2883
- */
2884
- declare function updatePendingDeposit(commitment: string, updates: Partial<PendingDeposit>): void;
2885
- /**
2886
- * Remove a pending deposit (e.g., after successful confirmation)
2887
- */
2888
- declare function removePendingDeposit(commitment: string): void;
2889
- /**
2890
- * Clear all pending deposits
2891
- */
2892
- declare function clearPendingDeposits(): void;
2893
- /**
2894
- * Save a pending withdrawal
2895
- * Call this when you receive the request_id from the relay
2896
- */
2897
- declare function savePendingWithdrawal(withdrawal: PendingWithdrawal): void;
2898
- /**
2899
- * Load all pending withdrawals
2900
- */
2901
- declare function loadPendingWithdrawals(): PendingWithdrawal[];
2902
- /**
2903
- * Update a pending withdrawal status
2904
- */
2905
- declare function updatePendingWithdrawal(requestId: string, updates: Partial<PendingWithdrawal>): void;
2906
- /**
2907
- * Remove a pending withdrawal (e.g., after successful completion)
2908
- */
2909
- declare function removePendingWithdrawal(requestId: string): void;
2910
- /**
2911
- * Clear all pending withdrawals
2912
- */
2913
- declare function clearPendingWithdrawals(): void;
2914
- /**
2915
- * Check if there are any pending operations that need recovery
2916
- * Call this on page load to determine if recovery UI should be shown
2917
- */
2918
- declare function hasPendingOperations(): boolean;
2919
- /**
2920
- * Get summary of pending operations for recovery UI
2921
- */
2922
- declare function getPendingOperationsSummary(): {
2923
- deposits: PendingDeposit[];
2924
- withdrawals: PendingWithdrawal[];
2925
- totalPending: number;
2926
- };
2927
- /**
2928
- * Clean up stale pending operations
2929
- * Call this periodically to remove old failed/completed operations
2930
- *
2931
- * @param maxAgeMs Maximum age in milliseconds before an operation is removed (default: 24 hours)
2932
- */
2933
- declare function cleanupStalePendingOperations(maxAgeMs?: number): {
2934
- removedDeposits: number;
2935
- removedWithdrawals: number;
2936
- };
2937
-
2938
- /**
2939
- * UTXO Transaction Methods
2940
- *
2941
- * This module provides high-level methods for UTXO transactions:
2942
- * - transact(): Core UTXO transaction
2943
- * - transfer(): Shield-to-shield transfer
2944
- * - partialWithdraw(): Withdraw with change
2945
- */
2946
-
2947
- declare const DEFAULT_TRANSACTION_CIRCUITS_URL = "https://storage.googleapis.com/cloak-circuits/circuits/0.1.0";
2948
- /**
2949
- * Set circuits base path: local directory containing `transaction_js/` and `transaction_final.zkey`,
2950
- * or an `http(s)` base URL to those artifacts (Node will download once per process to a temp dir).
2951
- */
2952
- declare function setCircuitsPath(next: string): void;
2953
- /**
2954
- * Get the current circuits path
2955
- */
2956
- declare function getCircuitsPath(): string;
2957
- declare function computeChainNoteHash(publicAmount: bigint, extDataHash: bigint, noteTimestamp: bigint, noteCommitment: bigint): Promise<bigint>;
2958
- /**
2959
- * Compute extDataHash for binding proof to external parameters
2960
- *
2961
- * extDataHash = Poseidon(recipient, relayerFee, relayer)
2962
- */
2963
- declare function computeExtDataHash(recipient: PublicKey | null, relayerFee: bigint, relayer: PublicKey | null): Promise<bigint>;
3183
+ declare function computeChainNoteHash(publicAmount: bigint, extDataHash: bigint, noteTimestamp: bigint, noteCommitment: bigint, noteSalt: bigint, outAmount0: bigint, outPubkey0: bigint, noteIsSendToSelfKey0: bigint): Promise<bigint>;
3184
+ declare function computeExtDataHash(recipient: PublicKey | null, relayerFee: bigint, relayer: PublicKey | null, maxFee?: bigint): Promise<bigint>;
2964
3185
  /**
2965
3186
  * Options for transact operation
2966
3187
  */
@@ -2978,8 +3199,8 @@ interface TransactOptions {
2978
3199
  /** Relayer address for fee payment */
2979
3200
  relayer?: PublicKey;
2980
3201
  /**
2981
- * Relay URL override.
2982
- * Defaults to `https://api.cloak.ag` when omitted.
3202
+ * Relay URL. Never defaulted: resolved from this option, then from `CLOAK_RELAY_URL`.
3203
+ * Name production explicitly (`https://api.cloak.ag`).
2983
3204
  */
2984
3205
  relayUrl?: string;
2985
3206
  /** Keypair of the depositor (signs the deposit transaction) - for programmatic use */
@@ -3065,6 +3286,13 @@ interface TransactOptions {
3065
3286
  * Each entry must be base64-encoded note bytes.
3066
3287
  */
3067
3288
  encryptedNotes?: string[];
3289
+ /**
3290
+ * Omit the on-chain encrypted chain-note envelope entirely. The chainNoteHash public input
3291
+ * is unaffected (proof still binds it); only the optional ciphertext blob the program emits
3292
+ * for wallet auto-scan is dropped. Use when the caller tracks UTXOs out-of-band and needs the
3293
+ * smaller packet — e.g. V3 + SPL deposits that would otherwise overflow the 1232-byte limit.
3294
+ */
3295
+ disableChainNotes?: boolean;
3068
3296
  /**
3069
3297
  * Optional nk (32 bytes, hex or bytes) for diversified chain note encryption (Phase 3).
3070
3298
  * When provided, 2 per-output encrypted notes are embedded on-chain.
@@ -3076,6 +3304,32 @@ interface TransactOptions {
3076
3304
  * passing nk at every call site — e.g. pass a getter from your key manager.
3077
3305
  */
3078
3306
  getChainNoteViewingKeyNk?: () => Promise<string | Uint8Array | null>;
3307
+ /**
3308
+ * Pin the chain note's 96-bit `noteSalt` instead of drawing a fresh one (VK-01).
3309
+ *
3310
+ * Pass the `noteSalt` returned by {@link createRecoverableDepositUtxo}. That helper derives the
3311
+ * deposit note's keypair and blinding as `PRF(nk, noteSalt)`, and the chain note is the ONLY place
3312
+ * the salt is published — so a cold `(rpc, programId, nk)` scan can only rebuild the note if the
3313
+ * salt that anchored it is the salt that reaches the note. Leaving this unset draws a random salt,
3314
+ * which is correct for every non-derived flow.
3315
+ *
3316
+ * Requires an explicit `chainNoteViewingKeyNk` / `getChainNoteViewingKeyNk`; `transact` refuses
3317
+ * otherwise rather than pairing the salt with an nk inferred from the note's own key.
3318
+ */
3319
+ chainNoteSalt?: bigint;
3320
+ /**
3321
+ * The RECIPIENT's 32-byte X25519 public viewing key, for a shield-to-shield send.
3322
+ *
3323
+ * Supply `deriveViewingKeyFromNk(recipientNk).publicKey` (hex or bytes). When present, the SDK
3324
+ * seals `{amount, blinding}` of output 0 to this key and ships it as `recipient_delivery_notes`,
3325
+ * which the relay publishes as a CLKD1 carrier the recipient can find with their `nk` alone.
3326
+ *
3327
+ * When absent, the send still succeeds but the recipient CANNOT discover the note from chain —
3328
+ * it must be handed over out of band. That was the shipped behaviour and is what campaign rows
3329
+ * S2S-08 / VK-01 measured. Ignored on deposits and withdrawals: the relay rejects the field
3330
+ * outright on anything that is not a send.
3331
+ */
3332
+ recipientViewingPublicKey?: Uint8Array | string;
3079
3333
  /**
3080
3334
  * Cached Merkle tree from a previous transaction.
3081
3335
  * When provided, the SDK skips fetching commitments from the relay and uses this
@@ -3151,17 +3405,119 @@ interface RiskQuoteInstructionResponse {
3151
3405
  data: string;
3152
3406
  };
3153
3407
  }
3154
- /**
3155
- * Fetch the risk quote instruction from a backend (e.g. relay /risk-quote).
3156
- * The backend should call Switchboard's fetchQuoteIx for the given wallet
3157
- * (Range Risk API) and return the serialized instruction.
3158
- * See: https://www.range.org/blog/integrate-range-onchain-risk-verifier-into-your-solana-program
3159
- */
3160
- declare function fetchRiskQuoteInstruction(riskQuoteUrl: string, wallet: PublicKey, options?: {
3408
+ declare function fetchRiskQuoteInstruction(riskQuoteUrl: string, wallet: PublicKey, options: {
3409
+ poolMint: PublicKey;
3161
3410
  recipient?: PublicKey;
3162
3411
  amount?: bigint;
3163
3412
  token?: PublicKey;
3413
+ commitment?: string | Uint8Array;
3414
+ context?: "deposit" | "send" | "withdraw";
3415
+ nullifier0?: string;
3416
+ nullifier1?: string;
3164
3417
  }): Promise<TransactionInstruction>;
3418
+ /** Everything the `/transact` relay body is built from. */
3419
+ interface TransactRequestBodyParams {
3420
+ proofB64: string;
3421
+ publicInputsB64: string;
3422
+ mint: PublicKey;
3423
+ recipient?: PublicKey | null;
3424
+ relayer?: PublicKey | null;
3425
+ relayerFee: bigint;
3426
+ maxFee: bigint;
3427
+ encryptedNotes?: string[];
3428
+ riskQuote?: {
3429
+ signature: string;
3430
+ message: string;
3431
+ signer_pubkey: string;
3432
+ };
3433
+ /** Present only on shield-to-shield sends, where the relay enforces sanctions on the sender. */
3434
+ sender?: PublicKey | null;
3435
+ /** Signed public amount; zero is a shield-to-shield send. */
3436
+ externalAmount: bigint;
3437
+ /** Base64 CLKD1 envelopes, from `buildRecipientDeliveryNotes`. Omitted when the rail is off. */
3438
+ recipientDeliveryNotes?: string[];
3439
+ }
3440
+ /**
3441
+ * Assemble the `/transact` body.
3442
+ *
3443
+ * Extracted so the wire shape is testable without a proof. The field set is a contract with
3444
+ * `services/relay/src/api/transact.rs`, and `recipient_delivery_notes` in particular is part of the
3445
+ * relay's signed field view (`TRANSACT_AUTH_FIELDS`) — so a body that carries it must be signed
3446
+ * with it present, and a body that omits it must be signed with an explicit null. `buildAuthRequest`
3447
+ * handles that, but only if the field is genuinely absent rather than set to `undefined`-ish values.
3448
+ */
3449
+ declare function buildTransactRequestBody(params: TransactRequestBodyParams): Record<string, unknown>;
3450
+ /** Everything settlement verification needs to check THIS proof against chain state. */
3451
+ interface SettlementContext {
3452
+ connection: SettlementConnection;
3453
+ programId: PublicKey;
3454
+ mint: PublicKey;
3455
+ /** The proof's public input nullifiers — the ground truth for "did this land". */
3456
+ inputNullifiers: bigint[];
3457
+ }
3458
+ type RelaySubmissionResult = {
3459
+ kind: "submitted";
3460
+ signature: string;
3461
+ commitmentIndices?: [number, number];
3462
+ viewingKeyRegistered?: boolean;
3463
+ settlement: SettlementVerdict;
3464
+ }
3465
+ /** The relay rejected the proof's root; the caller must rebuild the tree and re-prove. */
3466
+ | {
3467
+ kind: "stale-root";
3468
+ error: Error;
3469
+ } | {
3470
+ kind: "failed";
3471
+ error: Error;
3472
+ };
3473
+ interface SubmitTransactToRelayArgs {
3474
+ relayUrl: string;
3475
+ /** The exact body to POST. Auth fields are added ONCE, in place, and then never changed. */
3476
+ requestBody: Record<string, unknown>;
3477
+ programId: PublicKey;
3478
+ depositorKeypair?: Keypair;
3479
+ settlement: SettlementContext;
3480
+ /** True while the caller still has a re-prove budget for a stale root. */
3481
+ canRetryStaleRoot: boolean;
3482
+ maxNetworkRetries?: number;
3483
+ requestTimeoutMs?: number;
3484
+ /** Injection seam for tests; defaults to global fetch. */
3485
+ fetchImpl?: typeof fetch;
3486
+ onProgress?: (status: string) => void;
3487
+ /** Total window allowed for the on-chain settlement check after a reported success. */
3488
+ settlementTimeoutMs?: number;
3489
+ /** Shorter window used to ask "did it land anyway?" after a terminal relay failure. */
3490
+ failureProbeTimeoutMs?: number;
3491
+ }
3492
+ /**
3493
+ * POST one logical `/transact` request and REPORT ONLY WHAT THE CHAIN CONFIRMS.
3494
+ *
3495
+ * Extracted from `transact()` so the two campaign defects it fixes are reproducible without a
3496
+ * validator: a stub `fetchImpl` plays the hostile relay, a stub `SettlementConnection` plays the
3497
+ * RPC. Its control flow is the caller's old inline network-retry loop, unchanged except where
3498
+ * noted below.
3499
+ *
3500
+ * THREE fixes live here, all of them the same root cause — trusting the counterparty:
3501
+ *
3502
+ * 1. REL-A-1 (root cause of REL-A-10): the request is signed ONCE, before the retry loop, and
3503
+ * every retry re-POSTs the byte-identical body. The relay's recovery contract is keyed on
3504
+ * `(auth_nonce, endpoint, sender, request_digest)` — `request_auth.rs::authenticate_relay_request`
3505
+ * — and `handle_transact` answers an exact replay from its durable row: `Completed` replays the
3506
+ * stored response, `Prepared` reconciles the stored signature against chain, `Processing`
3507
+ * returns a retryable 503. The shipped client re-signed with a FRESH `randomUUID()` nonce inside
3508
+ * the loop, so every retry was a NEW request that could only collide with its own predecessor's
3509
+ * nullifier reservation (503) — measured: 7 POSTs, 7 nonces, byte-identical business fields, and
3510
+ * the recovery path unreachable. Re-signing here is not a fallback: a nonce that has aged past
3511
+ * the relay's 300s freshness window is still accepted for an EXACT replay whose row exists, and
3512
+ * when no row exists nothing was mutated, so the 401 is both correct and safe.
3513
+ * 2. X-S-01B: a reported success is verified against chain before it is returned. The relay's word
3514
+ * plus its commitment indices are not evidence; the input nullifier PDAs are.
3515
+ * 3. REL-A-10 / (b): the signature the relay puts in a `submission_outcome_unknown` body is
3516
+ * captured across attempts, and every terminal failure that could plausibly have been submitted
3517
+ * is resolved against chain into a `SettlementVerificationError` that states the outcome and
3518
+ * carries the signature.
3519
+ */
3520
+ declare function submitTransactToRelay(args: SubmitTransactToRelayArgs): Promise<RelaySubmissionResult>;
3165
3521
  declare function transact(params: TransactParams, options: TransactOptions): Promise<TransactResult>;
3166
3522
  /**
3167
3523
  * Execute a shield-to-shield transfer
@@ -3226,6 +3582,25 @@ interface UtxoSwapResult extends TransactResult {
3226
3582
  nullifier: string;
3227
3583
  /** Relay request ID for swap execution status */
3228
3584
  requestId?: string;
3585
+ /**
3586
+ * V3-04 refund-fallback secret. **Persist this** to recover the swap principal if the swap times
3587
+ * out before TX2. On a timeout close the program builds the amount-bound note
3588
+ * R_fb = Poseidon(amountAfterFee, publicKey, blinding, WSOL)
3589
+ * where `amountAfterFee` is the principal it locked (read authoritatively from the on-chain
3590
+ * `SwapState.sol_amount`, = gross swap amount minus the on-chain swap fee). `ClaimRefund` then spends
3591
+ * `R_fb` with `privateKey` (a plain ZK membership proof of the refund tree). All values are hex.
3592
+ *
3593
+ * VK-02: when `derivedFromNk` is true this secret is ALSO recoverable from the wallet's `nk` plus
3594
+ * the swap's public first input nullifier — see `matchSwapRefundLeaf`. Persisting it is still the
3595
+ * fast path; losing it is no longer terminal. When false (no `nk` was supplied to the swap) this
3596
+ * object is the only copy that will ever exist.
3597
+ */
3598
+ refund: {
3599
+ privateKey: string;
3600
+ publicKey: string;
3601
+ blinding: string;
3602
+ derivedFromNk: boolean;
3603
+ };
3229
3604
  }
3230
3605
  /**
3231
3606
  * Execute a UTXO swap withdrawal
@@ -3255,24 +3630,755 @@ declare function swapUtxo(params: UtxoSwapParams, options: TransactOptions): Pro
3255
3630
  declare function swapWithChange(inputUtxos: Utxo[], swapAmount: bigint, outputMint: PublicKey, recipientAta: PublicKey, minOutputAmount: bigint, options: TransactOptions, recipientWallet?: PublicKey): Promise<UtxoSwapResult>;
3256
3631
 
3257
3632
  /**
3258
- * Compact deterministic chain note format (current format only).
3633
+ * Direct Circom WASM Proof Generation
3634
+ *
3635
+ * This module provides direct proof generation using snarkjs and Circom WASM,
3636
+ * matching the approach used in services-new/tests/src/proof.ts
3637
+ *
3638
+ * Artifacts come from the pinned per-bundle hosts in `config/circuit-release`,
3639
+ * verified by digest; no backend prover service is required.
3640
+ */
3641
+
3642
+ /**
3643
+ * Default base URL for the legacy `withdraw_regular` / `withdraw_swap` artifacts.
3644
+ *
3645
+ * Derived from {@link LEGACY_WITHDRAW_CIRCUIT_BUNDLE}, so the version segment is
3646
+ * the same one the pinned digests were declared under. Do not write this URL out
3647
+ * by hand anywhere — change the bundle instead.
3648
+ *
3649
+ * This is NOT the base for the ceremony-frozen `transaction` circuit: that
3650
+ * circuit lives in a different bundle ({@link TRANSACTION_CIRCUIT_BUNDLE}) with
3651
+ * different digests, and is configured through `setCircuitsPath()`.
3652
+ *
3653
+ * `string | null` — null when the bundle has no published host, which is the
3654
+ * case for the legacy withdraw artifacts.
3655
+ *
3656
+ * RESOLVED LAZILY ON PURPOSE. This was previously
3657
+ * `requirePinnedBaseUrl('withdraw_regular')` evaluated at module scope, so an
3658
+ * unpinned bundle made merely IMPORTING the SDK throw — breaking every consumer,
3659
+ * including those that never touch a legacy withdraw path. The diagnostic is
3660
+ * still raised, by `resolveCircuitsUrl` and `getDefaultCircuitsPath` at the
3661
+ * point of use, where a caller can actually act on it.
3662
+ */
3663
+ declare const DEFAULT_CIRCUITS_URL: string | null;
3664
+ interface WithdrawRegularInputs {
3665
+ root: bigint;
3666
+ nullifier: bigint;
3667
+ outputs_hash: bigint;
3668
+ public_amount: bigint;
3669
+ amount: bigint;
3670
+ leaf_index: bigint;
3671
+ sk: [bigint, bigint];
3672
+ r: [bigint, bigint];
3673
+ pathElements: bigint[];
3674
+ pathIndices: number[];
3675
+ num_outputs: number;
3676
+ out_addr: bigint[][];
3677
+ out_amount: bigint[];
3678
+ out_flags: number[];
3679
+ var_fee: bigint;
3680
+ rem: bigint;
3681
+ }
3682
+ interface WithdrawSwapInputs {
3683
+ sk_spend: bigint;
3684
+ r: bigint;
3685
+ amount: bigint;
3686
+ leaf_index: bigint;
3687
+ path_elements: bigint[];
3688
+ path_indices: number[];
3689
+ root: bigint;
3690
+ nullifier: bigint;
3691
+ outputs_hash: bigint;
3692
+ public_amount: bigint;
3693
+ input_mint: bigint[];
3694
+ output_mint: bigint[];
3695
+ recipient_ata: bigint[];
3696
+ min_output_amount: bigint;
3697
+ var_fee: bigint;
3698
+ rem: bigint;
3699
+ }
3700
+ interface ProofResult {
3701
+ proof: Groth16Proof;
3702
+ publicSignals: string[];
3703
+ proofBytes: Uint8Array;
3704
+ publicInputsBytes: Uint8Array;
3705
+ }
3706
+ /**
3707
+ * Generate Groth16 proof for regular withdrawal using Circom WASM
3708
+ *
3709
+ * This matches the approach in services-new/tests/src/proof.ts
3710
+ *
3711
+ * @param inputs - Circuit inputs
3712
+ * @param circuitsPath - Ignored. Proof generation always uses pinned S3 circuits.
3713
+ */
3714
+ declare function generateWithdrawRegularProof(inputs: WithdrawRegularInputs, circuitsPath: string): Promise<ProofResult>;
3715
+ /**
3716
+ * Generate Groth16 proof for swap withdrawal using Circom WASM
3717
+ *
3718
+ * This matches the approach in services-new/tests/src/proof.ts
3719
+ *
3720
+ * @param inputs - Circuit inputs
3721
+ * @param circuitsPath - Ignored. Proof generation always uses pinned S3 circuits.
3722
+ */
3723
+ declare function generateWithdrawSwapProof(inputs: WithdrawSwapInputs, circuitsPath: string): Promise<ProofResult>;
3724
+ /**
3725
+ * Check if circuits are available from the pinned S3 source.
3726
+ */
3727
+ declare function areCircuitsAvailable(circuitsPath: string): Promise<boolean>;
3728
+ /**
3729
+ * Get default circuits URL.
3730
+ */
3731
+ declare function getDefaultCircuitsPath(): Promise<string>;
3732
+ /**
3733
+ * Pinned circuit artifact hashes (SHA-256), flattened from the release table in
3734
+ * `config/circuit-release.ts`.
3259
3735
  *
3260
- * Envelope: [version 1][ciphertext (timestamp + tag)]
3736
+ * This is a *view*, not the source of truth: edit the bundle, not this object.
3737
+ * It stays writable because the artifact TOCTOU tests re-pin it to synthetic
3738
+ * fixtures; production code must not mutate it.
3739
+ */
3740
+ declare const EXPECTED_CIRCUIT_HASHES: {
3741
+ withdraw_regular_wasm: string;
3742
+ withdraw_regular_zkey: string;
3743
+ withdraw_swap_wasm: string;
3744
+ withdraw_swap_zkey: string;
3745
+ transaction_wasm: string;
3746
+ transaction_zkey: string;
3747
+ };
3748
+ /**
3749
+ * Circuit verification result
3750
+ */
3751
+ interface CircuitVerificationResult {
3752
+ /** Whether verification passed */
3753
+ valid: boolean;
3754
+ /** Which circuit was checked */
3755
+ circuit: CircuitName;
3756
+ /** Error message if verification failed */
3757
+ error?: string;
3758
+ computed?: {
3759
+ wasm: string;
3760
+ zkey: string;
3761
+ };
3762
+ expected?: {
3763
+ wasm: string;
3764
+ zkey: string;
3765
+ };
3766
+ }
3767
+ /** Circuit artifact bytes together with the digests computed over those bytes. */
3768
+ interface VerifiedCircuitArtifacts {
3769
+ /** `<circuit>_js/<circuit>.wasm` bytes. */
3770
+ wasm: Uint8Array;
3771
+ /** `<circuit>_final.zkey` bytes. */
3772
+ zkey: Uint8Array;
3773
+ /** SHA-256 (lowercase hex) of the buffers in this object. */
3774
+ digests: {
3775
+ wasm: string;
3776
+ zkey: string;
3777
+ };
3778
+ }
3779
+ /**
3780
+ * Load circuit artifacts and return the very bytes whose digest was checked.
3781
+ *
3782
+ * This is the only supported way to obtain proving artifacts: the caller passes
3783
+ * the returned buffers straight to `snarkjs.groth16.fullProve`, which accepts a
3784
+ * `Uint8Array` for both the wasm and the zkey. Passing snarkjs a URL or a file
3785
+ * path instead would re-read the artifact independently of the digest check, so
3786
+ * a CDN (or a concurrent writer on disk) could serve good bytes to the check and
3787
+ * different bytes to the prover.
3788
+ *
3789
+ * Fails closed: any digest mismatch, unreachable artifact, or environment that
3790
+ * cannot produce bytes (a browser pointed at a local directory) throws rather
3791
+ * than falling back to an unverified source.
3792
+ */
3793
+ declare function loadVerifiedCircuitArtifacts(circuitsPath: string | null, circuit: CircuitName): Promise<VerifiedCircuitArtifacts>;
3794
+ /**
3795
+ * Report whether a circuit's artifacts match the digests pinned in this SDK.
3796
+ *
3797
+ * This is a *reporting* helper (used by `verifyAllCircuits` for start-up checks
3798
+ * and diagnostics). It is NOT what gates proving: a check that only inspects the
3799
+ * source cannot say anything about the bytes a later, independent read returns.
3800
+ * Proof paths must call {@link loadVerifiedCircuitArtifacts} and hand the bytes
3801
+ * it returns to snarkjs.
3802
+ *
3803
+ * IMPORTANT: If the hashes don't match, the circuit may produce proofs
3804
+ * that will be rejected by the on-chain verifier!
3805
+ *
3806
+ * @param circuitsPath - Ignored for the legacy withdraw circuits (they always use
3807
+ * their own pinned bundle); honoured for `transaction`.
3808
+ * @param circuit - Which circuit to verify
3809
+ * @returns Verification result
3810
+ *
3811
+ * @example
3812
+ * ```typescript
3813
+ * const result = await verifyCircuitIntegrity(DEFAULT_CIRCUITS_URL, 'withdraw_regular');
3814
+ * if (!result.valid) {
3815
+ * console.error('Circuit verification failed:', result.error);
3816
+ * // Don't proceed with proof generation!
3817
+ * }
3818
+ * ```
3819
+ */
3820
+ declare function verifyCircuitIntegrity(circuitsPath: string | null, circuit: CircuitName, prefetched?: {
3821
+ wasm: Uint8Array;
3822
+ zkey: Uint8Array;
3823
+ }): Promise<CircuitVerificationResult>;
3824
+ /**
3825
+ * Assert the ceremony-frozen `transaction` circuit artifacts are the pinned ones.
3826
+ *
3827
+ * Throws (fail-closed) when the digests do not match, mirroring how
3828
+ * `generateWithdrawRegularProof` / `generateWithdrawSwapProof` gate the legacy
3829
+ * circuits. Proving against an unpinned zkey silently produces proofs the
3830
+ * on-chain verifying key rejects, so failing here is strictly better than
3831
+ * failing on-chain.
3832
+ *
3833
+ * @param circuitsPath - Base directory or URL holding `transaction_js/transaction.wasm`
3834
+ * and `transaction_final.zkey`.
3835
+ * @param prefetched - Already-downloaded artifact bytes, to avoid a second fetch.
3836
+ */
3837
+ declare function assertTransactionCircuitIntegrity(circuitsPath: string | null, prefetched?: {
3838
+ wasm: Uint8Array;
3839
+ zkey: Uint8Array;
3840
+ }): Promise<void>;
3841
+ /**
3842
+ * Verify all circuits before use
3843
+ *
3844
+ * Call this at SDK initialization to ensure circuits are valid.
3845
+ *
3846
+ * @param circuitsPath - For the legacy withdraw circuits this is ignored; verification
3847
+ * always uses their own pinned bundle.
3848
+ * @param transactionCircuitsPath - Base for the ceremony-frozen `transaction` circuit.
3849
+ * Pass `getCircuitsPath()` when the caller has reconfigured it;
3850
+ * `null` reports the "no base configured" state rather than throwing.
3851
+ * @returns Array of verification results (one per circuit)
3852
+ */
3853
+ declare function verifyAllCircuits(circuitsPath: string, transactionCircuitsPath?: string | null): Promise<CircuitVerificationResult[]>;
3854
+
3855
+ /**
3856
+ * Pending Operations Manager
3857
+ *
3858
+ * Utility for persisting pending deposit/withdrawal operations in browser storage.
3859
+ * This enables recovery if the browser crashes or user navigates away mid-operation.
3860
+ *
3861
+ * IMPORTANT: This uses localStorage by default which has security implications.
3862
+ * Notes contain sensitive spending keys - consider using more secure storage
3863
+ * in production (e.g., encrypted IndexedDB, secure enclave).
3864
+ */
3865
+
3866
+ /**
3867
+ * Pending deposit record
3868
+ */
3869
+ interface PendingDeposit {
3870
+ /** The note (contains spending secrets!) */
3871
+ note: CloakNote;
3872
+ /** When the deposit was initiated */
3873
+ startedAt: number;
3874
+ /** Transaction signature if available */
3875
+ txSignature?: string;
3876
+ /** Status of the deposit */
3877
+ status: "pending" | "tx_sent" | "confirmed" | "failed";
3878
+ /** Error message if failed */
3879
+ error?: string;
3880
+ }
3881
+ /**
3882
+ * Pending withdrawal record
3883
+ */
3884
+ interface PendingWithdrawal {
3885
+ /** The relay request ID (for resumption) */
3886
+ requestId: string;
3887
+ /** The note commitment being withdrawn */
3888
+ commitment: string;
3889
+ /** The nullifier being used */
3890
+ nullifier: string;
3891
+ /** When the withdrawal was initiated */
3892
+ startedAt: number;
3893
+ /** Status of the withdrawal */
3894
+ status: "pending" | "processing" | "completed" | "failed";
3895
+ /** Transaction signature if completed */
3896
+ txSignature?: string;
3897
+ /** Error message if failed */
3898
+ error?: string;
3899
+ }
3900
+ /**
3901
+ * Save a pending deposit
3902
+ * Call this BEFORE sending the on-chain transaction to ensure note is persisted
3903
+ */
3904
+ declare function savePendingDeposit(deposit: PendingDeposit): void;
3905
+ /**
3906
+ * Load all pending deposits
3907
+ */
3908
+ declare function loadPendingDeposits(): PendingDeposit[];
3909
+ /**
3910
+ * Update a pending deposit status
3911
+ */
3912
+ declare function updatePendingDeposit(commitment: string, updates: Partial<PendingDeposit>): void;
3913
+ /**
3914
+ * Remove a pending deposit (e.g., after successful confirmation)
3915
+ */
3916
+ declare function removePendingDeposit(commitment: string): void;
3917
+ /**
3918
+ * Clear all pending deposits
3919
+ */
3920
+ declare function clearPendingDeposits(): void;
3921
+ /**
3922
+ * Save a pending withdrawal
3923
+ * Call this when you receive the request_id from the relay
3924
+ */
3925
+ declare function savePendingWithdrawal(withdrawal: PendingWithdrawal): void;
3926
+ /**
3927
+ * Load all pending withdrawals
3928
+ */
3929
+ declare function loadPendingWithdrawals(): PendingWithdrawal[];
3930
+ /**
3931
+ * Update a pending withdrawal status
3932
+ */
3933
+ declare function updatePendingWithdrawal(requestId: string, updates: Partial<PendingWithdrawal>): void;
3934
+ /**
3935
+ * Remove a pending withdrawal (e.g., after successful completion)
3936
+ */
3937
+ declare function removePendingWithdrawal(requestId: string): void;
3938
+ /**
3939
+ * Clear all pending withdrawals
3940
+ */
3941
+ declare function clearPendingWithdrawals(): void;
3942
+ /**
3943
+ * Check if there are any pending operations that need recovery
3944
+ * Call this on page load to determine if recovery UI should be shown
3945
+ */
3946
+ declare function hasPendingOperations(): boolean;
3947
+ /**
3948
+ * Get summary of pending operations for recovery UI
3949
+ */
3950
+ declare function getPendingOperationsSummary(): {
3951
+ deposits: PendingDeposit[];
3952
+ withdrawals: PendingWithdrawal[];
3953
+ totalPending: number;
3954
+ };
3955
+ /**
3956
+ * Clean up stale pending operations
3957
+ * Call this periodically to remove old failed/completed operations
3958
+ *
3959
+ * @param maxAgeMs Maximum age in milliseconds before an operation is removed (default: 24 hours)
3960
+ */
3961
+ declare function cleanupStalePendingOperations(maxAgeMs?: number): {
3962
+ removedDeposits: number;
3963
+ removedWithdrawals: number;
3964
+ };
3965
+
3966
+ /**
3967
+ * Recipient-addressed delivery envelope (CLKD1).
3968
+ *
3969
+ * ── The defect this closes ────────────────────────────────────────────────────────────────────
3970
+ * A shield-to-shield send creates an output note OWNED BY THE RECIPIENT. The only discovery
3971
+ * artefact the SDK used to publish for it was the CLK1 compliance chain note, which is encrypted
3972
+ * under the SENDER's `nk` and keyed (HKDF salt) by the output commitment. That note is openable by
3973
+ * the sender and by nobody else — so the recipient's money sat on chain, valid and spendable, with
3974
+ * its owner unable to see it (campaign rows S2S-08 / VK-01).
3975
+ *
3976
+ * This envelope is the recipient's half. It is encrypted to the RECIPIENT's X25519 public viewing
3977
+ * key — the one derived by `deriveViewingKeyFromNk(nk)` — so a cold scan holding nothing but
3978
+ * `(rpc, programId, nk)` can open it. That triple is exactly the cold-scan contract VK-01 tested.
3979
+ *
3980
+ * ── Wire format — dictated by the relay, not by us ────────────────────────────────────────────
3981
+ * `services/relay/src/api/transact.rs:176-205` accepts EXACTLY ONE base64 envelope of EXACTLY
3982
+ * 112 bytes and only on a shield-to-shield send (`public_amount == 0`); anything else is a 400 and
3983
+ * no carrier is written. `services/relay/src/solana/mod.rs:2338` then publishes
3984
+ *
3985
+ * "CLKD1" || hex(output_commitment[0]) || hex(envelope)
3986
+ *
3987
+ * as an SPL Memo in a transaction that touches the PDA at seed `b"cloak_delivery_registry"`, which
3988
+ * is what makes it enumerable via `getSignaturesForAddress`.
3989
+ *
3990
+ * The 112 bytes are:
3991
+ *
3992
+ * [0 ..32) ephemeral X25519 public key
3993
+ * [32 ..56) 24-byte XSalsa20-Poly1305 nonce
3994
+ * [56..112) 56-byte ciphertext = 40-byte plaintext + 16-byte Poly1305 tag
3995
+ *
3996
+ * and the 40-byte plaintext is `amount(u64 LE) || blinding(u256 BE)` — precisely what a recipient
3997
+ * needs, alongside their own keypair and the pool mint, to recompute the commitment and spend it.
3998
+ * Amount is LE to match every other u64 the SDK writes (`chain-note.ts`, public inputs); blinding
3999
+ * is BE to match `bigintToBytes32` and the on-chain field-element convention.
4000
+ *
4001
+ * ── Crypto ────────────────────────────────────────────────────────────────────────────────────
4002
+ * X25519 ECDH + XSalsa20-Poly1305, i.e. `nacl.box`, reusing the exact construction already in
4003
+ * `core/keys.ts` (`nacl.box.before` + `nacl.secretbox`). No new primitive is introduced here.
4004
+ */
4005
+
4006
+ /** Ephemeral X25519 public key, at offset 0. */
4007
+ declare const RECIPIENT_DELIVERY_EPHEMERAL_PK_LEN = 32;
4008
+ /** XSalsa20-Poly1305 nonce, immediately after the ephemeral key. */
4009
+ declare const RECIPIENT_DELIVERY_NONCE_LEN = 24;
4010
+ /** amount(8) + blinding(32) — everything the owner needs to rebuild and spend the note. */
4011
+ declare const RECIPIENT_DELIVERY_PLAINTEXT_LEN = 40;
4012
+ /** Poly1305 authentication tag. */
4013
+ declare const RECIPIENT_DELIVERY_TAG_LEN = 16;
4014
+ /** Sealed payload = plaintext + tag. */
4015
+ declare const RECIPIENT_DELIVERY_CIPHERTEXT_LEN: number;
4016
+ /**
4017
+ * Total envelope size. The relay rejects any other length outright
4018
+ * (`RECIPIENT_DELIVERY_NOTE_BYTES` in `services/relay/src/api/transact.rs`), so this constant is a
4019
+ * contract with a shipped binary, not a preference.
4020
+ */
4021
+ declare const RECIPIENT_DELIVERY_NOTE_BYTES: number;
4022
+ /** PDA seed the relay derives the carrier's registry account from. */
4023
+ declare const DELIVERY_REGISTRY_SEED = "cloak_delivery_registry";
4024
+ /** ASCII tag prefixing the carrier memo payload. */
4025
+ declare const DELIVERY_MEMO_TAG = "CLKD1";
4026
+ /** The spendable contents of a delivery envelope. */
4027
+ interface RecipientDeliveryNote {
4028
+ /** Note amount in the pool mint's smallest unit. */
4029
+ amount: bigint;
4030
+ /** Note blinding factor (BN254 field element). */
4031
+ blinding: bigint;
4032
+ }
4033
+ /**
4034
+ * Seal `{amount, blinding}` to a recipient's X25519 public viewing key.
4035
+ *
4036
+ * Every input is length-checked here rather than at the relay: a malformed envelope costs a
4037
+ * confirmed transaction's worth of latency before the 400 comes back, and the send has already
4038
+ * generated its proof by then.
4039
+ */
4040
+ declare function encodeRecipientDeliveryNote(note: RecipientDeliveryNote, recipientViewingPublicKey: Uint8Array): Uint8Array;
4041
+ /**
4042
+ * Trial-open an envelope with a viewing secret. Returns `null` — never throws — when the envelope
4043
+ * is not ours, because a scanner runs this against every carrier in the registry.
4044
+ */
4045
+ declare function openRecipientDeliveryNote(envelope: Uint8Array, viewingSecretKey: Uint8Array): RecipientDeliveryNote | null;
4046
+ declare function recipientDeliveryNoteToBase64(envelope: Uint8Array): string;
4047
+ /** Parameters the send path already has in hand when it assembles the relay body. */
4048
+ interface BuildRecipientDeliveryNotesParams {
4049
+ /** Signed public amount. Zero — and only zero — is a shield-to-shield send. */
4050
+ externalAmount: bigint;
4051
+ /** External withdrawal recipient, if any. A send has none. */
4052
+ recipient?: PublicKey | null;
4053
+ /** The recipient's 32-byte X25519 public viewing key (`deriveViewingKeyFromNk(nk).publicKey`). */
4054
+ recipientViewingPublicKey?: Uint8Array | string | null;
4055
+ /**
4056
+ * The output note being delivered. MUST be output 0: the relay binds the carrier to
4057
+ * `public_inputs.output_commitments[0]` (`services/relay/src/api/transact.rs:1015`).
4058
+ */
4059
+ note?: Pick<Utxo, "amount" | "blinding"> | null;
4060
+ /** UTXO public key that owns `note` (i.e. `paddedOutputs[0].keypair.publicKey`). */
4061
+ noteOwnerPublicKey?: bigint;
4062
+ /** UTXO public key doing the spending (i.e. `paddedInputs[0].keypair.publicKey`). */
4063
+ spenderPublicKey?: bigint;
4064
+ }
4065
+ /**
4066
+ * Build the `recipient_delivery_notes` field for `/transact`, or `undefined` when the rail does not
4067
+ * apply. Returning `undefined` (rather than an empty array) matters: the relay's signed field view
4068
+ * treats omitted and null identically, and an empty array on a withdrawal would still be a 400.
4069
+ */
4070
+ declare function buildRecipientDeliveryNotes(params: BuildRecipientDeliveryNotesParams): string[] | undefined;
4071
+ /** Render the carrier memo byte-for-byte as `emit_recipient_delivery_carrier` does. */
4072
+ declare function encodeDeliveryCarrierMemo(outputCommitment: bigint | Uint8Array, envelope: Uint8Array): Uint8Array;
4073
+ interface ParsedDeliveryCarrier {
4074
+ /** Lowercase 64-char hex of the output commitment the carrier declares. */
4075
+ commitment: string;
4076
+ /** The raw 112-byte envelope. */
4077
+ note: Uint8Array;
4078
+ }
4079
+ /**
4080
+ * Parse one SPL Memo payload. Returns `null` for anything that is not a well-formed CLKD1 carrier —
4081
+ * the memo program accepts arbitrary UTF-8 from anyone, so this must fail closed on exact lengths
4082
+ * (I-12: exact `!==` gates, never `<`).
4083
+ */
4084
+ declare function parseDeliveryCarrierMemo(data: Uint8Array): ParsedDeliveryCarrier | null;
4085
+
4086
+ /**
4087
+ * Recoverable deposit notes (VK-01, deposit shape).
4088
+ *
4089
+ * ── The defect this closes ────────────────────────────────────────────────────────────────────
4090
+ * A cold scan holding exactly `(rpc, programId, nk)` — the contract the SDK advertises for viewing
4091
+ * keys — found the WITHDRAWAL change note and did NOT find the DEPOSIT. The control that makes the
4092
+ * miss attributable is the withdrawal: the same scanner, the same 300-signature window, the same
4093
+ * key, one shape found and one not.
4094
+ *
4095
+ * Two things were missing, and only one of them was visible from the failing arm:
4096
+ *
4097
+ * 1. `outPubkey0`. The deposit's chain note is v3 (timestamp + noteSalt), and `chainNoteHash` binds
4098
+ * `noteSemantics = Poseidon(outAmount0, outPubkey0, isSendToSelfKey0)`. `outPubkey0` is the
4099
+ * output note's OWNER key, which is not derivable from `nk` — the key hierarchy runs
4100
+ * `skSpend → nk` through BLAKE3 and does not run backwards. So the scanner could decrypt the
4101
+ * note and then had to drop it, because the hash it recomputed could never match. That is why
4102
+ * the widened arm needed an undocumented `ownUtxoPublicKey`: it was feeding the scanner the one
4103
+ * value the advertised contract does not carry.
4104
+ * 2. The BLINDING. Even with `ownUtxoPublicKey` the deposit is only VISIBLE, not RECOVERABLE — the
4105
+ * blinding came from `randomFieldElement()` inside `createUtxo` and is written nowhere. A note
4106
+ * you can see and cannot spend is not a recovered note.
4107
+ *
4108
+ * ── Why derivation, and not another envelope ──────────────────────────────────────────────────
4109
+ * The shielded-send shape was fixed with a CLKD1 delivery envelope because a send's output is owned
4110
+ * by SOMEONE ELSE: the only way to reach them is to encrypt to their key. A deposit's output is
4111
+ * SELF-owned. There is nobody to deliver to, and an envelope would cost 112 bytes on the one
4112
+ * transaction in the protocol that is wallet-signed and already tight against the 1232-byte packet
4113
+ * limit (a v4 chain note's extra 41 bytes is measured at 1282 and is exactly why deposits stay v3).
4114
+ *
4115
+ * So make the existing chain-note path recoverable instead. Both missing values become a PRF of the
4116
+ * wallet's own `nk` and the note salt the chain note ALREADY carries:
4117
+ *
4118
+ * seed = BLAKE3("cloak_deposit_note_v1" || nk || noteSalt(32B BE) || info)
4119
+ * privateKey = seed("sk") reduced into the field
4120
+ * blinding = seed("blinding") reduced into the field
4121
+ * publicKey = PoseidonEx(privateKey, KEYPAIR_) [L-02, matches keypair.circom]
4122
+ *
4123
+ * A cold scanner decrypts the chain note with `nk` (AES-GCM, HKDF-salted by the output commitment),
4124
+ * reads `noteSalt` out of the plaintext, replays those three lines, recomputes
4125
+ * `Poseidon(amount, publicKey, blinding, mint)` and requires it to equal a commitment the
4126
+ * transaction actually published. That equality is the authentication: it is not a heuristic, and a
4127
+ * wrong `nk` cannot produce it. Zero extra bytes on chain, no new envelope, no relay change.
4128
+ *
4129
+ * This is the same shape already blessed for swap timeout refunds in `core/swap-refund.ts`
4130
+ * (PRF(nk, nullifier0)), for the same reason: unrecoverable randomness becomes recoverable
4131
+ * randomness without changing anything an observer can see.
4132
+ *
4133
+ * ── The consequence to be explicit about ──────────────────────────────────────────────────────
4134
+ * The deposit note is owned by a PER-DEPOSIT key rather than by the wallet's single long-lived UTXO
4135
+ * keypair. It is still fully spendable — the wallet re-derives `privateKey` from `nk` whenever it
4136
+ * needs it — and it is strictly better for privacy, because `outPubkey0` no longer links a wallet's
4137
+ * deposits to each other. But it means the caller MUST persist (or be able to re-derive) `nk`, which
4138
+ * a Cloak wallet already does, and it means `transact` must be given an explicit `nk` rather than
4139
+ * inferring one from the output note's own key. `transact` enforces that rather than trusting it.
4140
+ *
4141
+ * ── Guardrails preserved ──────────────────────────────────────────────────────────────────────
4142
+ * [L-02] the public key comes from the capacity-tagged `derivePublicKey`. [H-04-shaped] a zero
4143
+ * public key or blinding would be an unspendable note, so the reduction forces non-zero and the
4144
+ * derivation refuses to return a degenerate pair. [M-04] `noteSalt` stays a private input to
4145
+ * `chainNoteHash`; it is only ever published inside the note's own authenticated ciphertext.
4146
+ */
4147
+
4148
+ /**
4149
+ * The chain note salt is 96 bits — the circuit constrains it with `Num2Bits(96)` and `transact`
4150
+ * generates exactly 12 bytes. A salt outside that range would produce a proof the circuit rejects,
4151
+ * so it is refused here rather than at proof time.
4152
+ */
4153
+ declare const CHAIN_NOTE_SALT_BITS = 96;
4154
+ /** The secrets a deposit note is built from — and the ones a cold scan re-derives. */
4155
+ interface DepositNoteSecrets {
4156
+ keypair: UtxoKeypair;
4157
+ blinding: bigint;
4158
+ }
4159
+ /** A deposit note recovered from `nk` alone, in spendable form. */
4160
+ interface RecoveredDepositNote extends DepositNoteSecrets {
4161
+ amount: bigint;
4162
+ mintAddress: PublicKey;
4163
+ /** The commitment the transaction published, reproduced from the derived secrets. */
4164
+ commitment: bigint;
4165
+ /** The salt the chain note carried, which anchored the derivation. */
4166
+ noteSalt: bigint;
4167
+ }
4168
+ /** A fresh 96-bit chain-note salt, from the same fail-closed source `transact` uses. */
4169
+ declare function randomDepositNoteSalt(): bigint;
4170
+ /**
4171
+ * Derive a deposit note's keypair and blinding from `(nk, noteSalt)`.
4172
+ *
4173
+ * Deterministic by design: this is the whole reason a cold scan can rebuild the note. Both the
4174
+ * builder and the scanner call it, so there is exactly one definition of what a deposit note is.
4175
+ */
4176
+ declare function deriveDepositNoteSecrets(viewingKeyNk: Uint8Array, noteSalt: bigint): Promise<DepositNoteSecrets>;
4177
+ /**
4178
+ * Build a deposit output note that a cold `(rpc, programId, nk)` scan can recover.
4179
+ *
4180
+ * Returns the UTXO to pass as `outputUtxos[0]` AND the salt that anchored it. The SAME salt must be
4181
+ * handed to `transact` as `options.chainNoteSalt`, because the chain note is what publishes it — a
4182
+ * salt that does not reach the note leaves the deposit exactly as undiscoverable as before.
4183
+ *
4184
+ * ```ts
4185
+ * const { utxo, noteSalt } = await createRecoverableDepositUtxo(amount, nk, mint);
4186
+ * await transact({ ..., outputUtxos: [utxo], externalAmount: amount },
4187
+ * { chainNoteViewingKeyNk: nk, chainNoteSalt: noteSalt, ... });
4188
+ * ```
4189
+ */
4190
+ declare function createRecoverableDepositUtxo(amount: bigint, viewingKeyNk: Uint8Array, mintAddress?: PublicKey, noteSalt?: bigint): Promise<{
4191
+ utxo: Utxo;
4192
+ noteSalt: bigint;
4193
+ }>;
4194
+ interface MatchDepositNoteParams {
4195
+ /** The scanning wallet's incoming viewing base. */
4196
+ viewingKeyNk: Uint8Array;
4197
+ /** `noteSalt`, read out of the decrypted chain note. */
4198
+ noteSalt: bigint;
4199
+ /** Candidate note amount — for a shield with no inputs this is the public deposit amount. */
4200
+ amount: bigint;
4201
+ /** Pool mint the commitment was computed under. */
4202
+ mintAddress: PublicKey;
4203
+ /** Output commitments the transaction actually published, hex or field elements. */
4204
+ outputCommitments: Array<string | bigint>;
4205
+ }
4206
+ /**
4207
+ * Decide whether a deposit's published commitment is one this `nk` can rebuild, and if so return
4208
+ * the note in spendable form. Returns `null` for everything that is not ours.
4209
+ *
4210
+ * The commitment equality is the authentication. Nothing here trusts the chain note's own claim
4211
+ * about what it describes; the note only supplies `noteSalt`, and the derived secrets have to
4212
+ * reproduce a value the transaction published or the candidate is discarded.
4213
+ */
4214
+ declare function matchDepositNote(params: MatchDepositNoteParams): Promise<RecoveredDepositNote | null>;
4215
+
4216
+ /**
4217
+ * Swap timeout-refund discovery (VK-02).
4218
+ *
4219
+ * ── The defect this closes ────────────────────────────────────────────────────────────────────
4220
+ * When a private swap exhausts its retry budget, `CloseSwapState` appends
4221
+ *
4222
+ * R_fb = Poseidon(amount_after_fee, refund_pubkey, refund_blinding, field(WSOL))
4223
+ *
4224
+ * to the main pool tree. `swapUtxo` generated `refund_pubkey` / `refund_blinding` from raw
4225
+ * randomness and returned them only in the in-memory `UtxoSwapResult.refund`. Lose that object —
4226
+ * a crashed tab, a different device, a scan from cold key material — and the leaf is real, funded,
4227
+ * and permanently unrecoverable: campaign row VK-02 observed R_fb under BOTH the refund note's own
4228
+ * viewing key and the swap creator's, and found nothing under either.
4229
+ *
4230
+ * ── The fix, and why it is SDK-only ───────────────────────────────────────────────────────────
4231
+ * Derive the refund authorization as a PRF of the owner's `nk` and the swap's own first input
4232
+ * nullifier:
4233
+ *
4234
+ * seed = BLAKE3("cloak_swap_refund_v1" || nk || nullifier0)
4235
+ *
4236
+ * `nullifier0` is published in the TransactSwap public inputs, so a cold scanner enumerating
4237
+ * program transactions can replay this derivation for every swap it sees, recompute R_fb, and
4238
+ * match it against the commitment `CloseSwapState` declared. `nk` is secret, so no third party can
4239
+ * predict, front-run or link the refund key — the on-chain artefacts are unchanged in shape and
4240
+ * every existing binding still holds.
4241
+ *
4242
+ * This changes NOTHING on chain or in the relay. `refund_pubkey` / `refund_blinding` remain
4243
+ * caller-supplied values bound into `computeSwapExtDataHash`; only their provenance changes, from
4244
+ * "unrecoverable randomness" to "recoverable randomness".
4245
+ *
4246
+ * ── Guardrails preserved ──────────────────────────────────────────────────────────────────────
4247
+ * [H-04 / DD-01] the derivation is rejected unless publicKey and blinding are both non-zero, and
4248
+ * it is unique per swap because `nullifier0` is unique per spend. [L-02] the public key comes from
4249
+ * the capacity-tagged `derivePublicKey`, matching `keypair.circom`.
4250
+ *
4251
+ * ── FORWARD-LOOKING ONLY ──────────────────────────────────────────────────────────────────────
4252
+ * Everything here — including `discoverSwapRefunds`, the RPC walker at the bottom of this file —
4253
+ * recovers only refunds whose authorization was DERIVED. Swaps already on chain whose refund keypair
4254
+ * and blinding came from raw randomness stay unrecoverable, permanently, by any key-only scan. There
4255
+ * is nothing to replay: those secrets existed only in the caller's in-memory `UtxoSwapResult.refund`.
4256
+ * Do not let this note get softened — a wallet that reports "no stranded refunds" on the strength of
4257
+ * an empty scan would be making a claim this code cannot support.
4258
+ */
4259
+
4260
+ /** A refund authorization: what `swapUtxo` binds into the swap ext-data hash. */
4261
+ interface SwapRefundAuthorization {
4262
+ privateKey: bigint;
4263
+ publicKey: bigint;
4264
+ blinding: bigint;
4265
+ }
4266
+ /** A refund leaf matched back to its owner, ready to spend. */
4267
+ interface RecoveredSwapRefund {
4268
+ keypair: UtxoKeypair;
4269
+ blinding: bigint;
4270
+ amount: bigint;
4271
+ /** R_fb as a field element, identical to the commitment `CloseSwapState` appended. */
4272
+ commitment: bigint;
4273
+ }
4274
+ /**
4275
+ * Derive a swap's refund authorization from the owner's `nk` and the swap's first input nullifier.
4276
+ *
4277
+ * Deterministic by design: this is what makes the refund leaf recoverable from key material alone.
4278
+ */
4279
+ declare function deriveSwapRefundAuthorization(viewingKeyNk: Uint8Array, inputNullifier: Uint8Array | bigint): Promise<SwapRefundAuthorization>;
4280
+ /**
4281
+ * R_fb, exactly as `CloseSwapState` computes it. The swap principal is always WSOL-locked, so the
4282
+ * mint term is the native-SOL sentinel.
4283
+ */
4284
+ declare function computeSwapRefundCommitment(amountAfterFee: bigint, refundPublicKey: bigint, refundBlinding: bigint): Promise<bigint>;
4285
+ interface MatchSwapRefundLeafParams {
4286
+ /** The scanning wallet's incoming viewing base. */
4287
+ viewingKeyNk: Uint8Array;
4288
+ /** First input nullifier of the candidate swap, read from its TransactSwap public inputs. */
4289
+ inputNullifier: Uint8Array | bigint;
4290
+ /** Principal returned to the pool, from the `cloak/refund_leaf/v1` event. */
4291
+ amountAfterFee: bigint;
4292
+ /** R_fb as appended on chain. */
4293
+ commitment: bigint | Uint8Array;
4294
+ }
4295
+ /**
4296
+ * Decide whether a public refund leaf belongs to the holder of `viewingKeyNk`, and if so return it
4297
+ * in spendable form. Returns `null` for every leaf that is not ours — a scanner runs this against
4298
+ * every close it can see.
4299
+ */
4300
+ declare function matchSwapRefundLeaf(params: MatchSwapRefundLeafParams): Promise<RecoveredSwapRefund | null>;
4301
+ /** One recovered refund leaf, with the chain coordinates that produced it. */
4302
+ interface DiscoveredSwapRefund extends RecoveredSwapRefund {
4303
+ /** Signature of the `CloseSwapState` transaction that appended the leaf. */
4304
+ signature: string;
4305
+ /** Leaf index, straight from the `cloak/refund_leaf/v1` event. */
4306
+ leafIndex: bigint;
4307
+ /** First input nullifier of the swap this refund belongs to (the PRF's second input). */
4308
+ inputNullifier: Uint8Array;
4309
+ /** Signature of the `TransactSwap` that opened the swap, when it was inside the scanned window. */
4310
+ swapSignature?: string;
4311
+ }
4312
+ interface DiscoverSwapRefundsOptions {
4313
+ /** Maximum program signatures to walk. Omit or 0 to walk the whole history. */
4314
+ limit?: number;
4315
+ /** Stop when this signature is reached (exclusive) — the cursor from a previous run. */
4316
+ untilSignature?: string;
4317
+ /** `getTransaction` concurrency (default 50). */
4318
+ batchSize?: number;
4319
+ /** Progress/status callback. */
4320
+ onStatus?: (status: string) => void;
4321
+ /**
4322
+ * Pool mint whose `swap_state` PDAs to derive. Swap input is wSOL-locked, so the default is the
4323
+ * native-SOL sentinel and there is no reason to change it outside a test.
4324
+ */
4325
+ poolMint?: PublicKey;
4326
+ }
4327
+ /**
4328
+ * Find every swap timeout-refund leaf on chain that belongs to the holder of `viewingKeyNk`, and
4329
+ * return each in spendable form (`keypair`, `blinding`, `amount`, `commitment`).
4330
+ *
4331
+ * Key material only — no relay, no local note store, no prior knowledge of the swap. This is the
4332
+ * recovery path for the exact situation VK-02 described: `CloseSwapState` appended a funded leaf and
4333
+ * its owner could not see it under any key they held.
4334
+ *
4335
+ * FORWARD-LOOKING ONLY. This finds refunds whose authorization was DERIVED as PRF(nk, nullifier0).
4336
+ * A swap whose refund keypair and blinding were drawn from raw randomness — every swap submitted
4337
+ * before that derivation shipped, and any swap built without an `nk` — has no derivation to replay,
4338
+ * and no key-only scan can ever recover it. Those secrets existed solely in the caller's in-memory
4339
+ * `UtxoSwapResult.refund`. An empty result is therefore NOT proof that a wallet has no stranded
4340
+ * refund; it means none of the leaves in the scanned window were derivable from this `nk`.
4341
+ *
4342
+ * @param connection Solana RPC connection.
4343
+ * @param programId Shield-pool program id.
4344
+ * @param viewingKeyNk 32-byte `nk` — the same value chain notes are decrypted with.
4345
+ */
4346
+ declare function discoverSwapRefunds(connection: Connection, programId: PublicKey, viewingKeyNk: Uint8Array, options?: DiscoverSwapRefundsOptions): Promise<DiscoveredSwapRefund[]>;
4347
+
4348
+ /**
4349
+ * Compact deterministic chain note format.
4350
+ *
4351
+ * Envelope: [version 1][ciphertext (plaintext + AES-GCM tag)]
4352
+ *
4353
+ * Plaintext layout by version:
4354
+ * - v3: [timestamp: u64 LE (8)][noteSalt: u256 BE (32)]
4355
+ * - v2: [timestamp: u64 LE (8)]
3261
4356
  */
3262
4357
  type ChainNoteTxType = "deposit" | "withdraw" | "transfer" | "swap" | "unknown";
3263
4358
  interface CompactChainNote {
3264
4359
  timestamp: bigint;
3265
4360
  commitment: string;
4361
+ noteSalt?: bigint;
4362
+ /** v4 only: the three terms that make up `noteSemantics`. */
4363
+ outAmount0?: bigint;
4364
+ outPubkey0?: bigint;
4365
+ isSendToSelfKey0?: bigint;
3266
4366
  }
3267
4367
  /**
3268
4368
  * Encrypt a compact deterministic chain note.
3269
- * Payload is minimal to keep relay withdrawals under Solana packet size limits.
3270
- * Envelope: [version 1][ciphertext (timestamp + tag)]
4369
+ *
4370
+ * v3 carries noteSalt so the recipient can recompute and verify the public
4371
+ * chainNoteHash without making the output commitment linkable by observers.
3271
4372
  */
3272
- declare function encryptCompactChainNote(timestamp: bigint, nk: Uint8Array, commitmentHex: string): Promise<Uint8Array>;
4373
+ declare function encryptCompactChainNote(timestamp: bigint, nk: Uint8Array, commitmentHex: string, noteSalt: bigint, semantics?: {
4374
+ outAmount0: bigint;
4375
+ outPubkey0: bigint;
4376
+ isSendToSelfKey0: bigint;
4377
+ }): Promise<Uint8Array>;
3273
4378
  /**
3274
4379
  * Decrypt a compact deterministic chain note with candidate output commitments.
3275
4380
  * Tries each commitment-derived key until AES-GCM authentication succeeds.
4381
+ * Accepts current v3 notes and legacy v2 notes.
3276
4382
  */
3277
4383
  declare function decryptCompactChainNote(noteBytes: Uint8Array, nk: Uint8Array, candidateCommitments: string[]): Promise<CompactChainNote>;
3278
4384
  declare function chainNoteToBase64(noteBytes: Uint8Array): string;
@@ -3341,6 +4447,30 @@ interface ScanResult {
3341
4447
  lastSignature?: string;
3342
4448
  /** Number of RPC getTransaction calls actually made (for diagnostics). */
3343
4449
  rpcCallsMade: number;
4450
+ /**
4451
+ * Notes delivered TO this wallet by other people's shield-to-shield sends, recovered from the
4452
+ * CLKD1 registry. These are SPENDABLE note secrets, not history records — they are intentionally
4453
+ * kept out of `transactions`/`summary` so the compliance report shape is unchanged.
4454
+ */
4455
+ deliveredNotes: DeliveredNote[];
4456
+ /**
4457
+ * This wallet's OWN deposits, rebuilt in SPENDABLE form from `nk` alone (VK-01).
4458
+ *
4459
+ * `transactions` records that a deposit happened and for how much. These carry the keypair and
4460
+ * blinding as well, which is the difference between seeing a note and being able to spend it.
4461
+ * Only deposits built with {@link createRecoverableDepositUtxo} appear here; a deposit whose
4462
+ * blinding came from raw randomness has nothing to rebuild and shows up as a history row only.
4463
+ *
4464
+ * Additive, for the same reason as `deliveredNotes`: note secrets are not compliance rows.
4465
+ */
4466
+ recoveredDepositNotes: RecoveredDepositNoteRecord[];
4467
+ }
4468
+ /** A recovered deposit note plus the chain coordinates it was recovered from. */
4469
+ interface RecoveredDepositNoteRecord extends RecoveredDepositNote {
4470
+ /** Signature of the deposit transaction. */
4471
+ signature: string;
4472
+ /** Millisecond timestamp from the chain note. */
4473
+ timestamp: bigint;
3344
4474
  }
3345
4475
  /** Options for `scanTransactions`. */
3346
4476
  interface ScanOptions {
@@ -3376,15 +4506,74 @@ interface ScanOptions {
3376
4506
  * matching the wallet's associated token account against the on-chain recipient ATA.
3377
4507
  */
3378
4508
  walletPublicKey?: string;
4509
+ /**
4510
+ * Sweep the CLKD1 recipient-delivery registry for notes sent TO this wallet (default: true).
4511
+ * Costs one extra `getSignaturesForAddress` page plus one `getTransaction` per carrier.
4512
+ */
4513
+ includeRecipientDeliveries?: boolean;
4514
+ /**
4515
+ * This wallet's UTXO public key. Supply it to authenticate each delivery carrier's declared
4516
+ * commitment (see `ScanRecipientDeliveryOptions.ownerUtxoPublicKey`). Without it the commitment
4517
+ * is reported unverified.
4518
+ */
4519
+ ownerUtxoPublicKey?: bigint;
4520
+ /** Candidate pool mints for delivery-carrier commitment verification. Defaults to native SOL. */
4521
+ deliveryMints?: PublicKey[];
4522
+ }
4523
+ /** A note delivered TO the scanning wallet by someone else's shield-to-shield send. */
4524
+ interface DeliveredNote {
4525
+ /** Output commitment (lowercase hex) the carrier declares. */
4526
+ commitment: string;
4527
+ /** Note amount, decrypted from the envelope. */
4528
+ amount: bigint;
4529
+ /** Note blinding, decrypted from the envelope. */
4530
+ blinding: bigint;
4531
+ /** Carrier transaction signature (the discovery record, NOT the source transaction). */
4532
+ carrierSignature: string;
4533
+ /** Carrier block time in seconds, when the RPC supplied one. */
4534
+ blockTime?: number;
4535
+ /**
4536
+ * Pool mint the note lives in, resolved by recomputing the commitment. Present only when
4537
+ * `ownerUtxoPublicKey` was supplied — without it the declared commitment cannot be checked.
4538
+ */
4539
+ mint?: string;
4540
+ /**
4541
+ * True when the declared commitment was recomputed from the decrypted note and matched. The
4542
+ * memo's commitment field is unauthenticated; only this recomputation binds it.
4543
+ */
4544
+ commitmentVerified: boolean;
4545
+ }
4546
+ interface ScanRecipientDeliveryOptions {
4547
+ connection: Connection;
4548
+ programId: PublicKey;
4549
+ /** nk (32 bytes). The X25519 opening key is `deriveViewingKeyFromNk(nk).privateKey`. */
4550
+ viewingKeyNk: Uint8Array;
4551
+ /**
4552
+ * The scanning wallet's UTXO public key. Supply it to authenticate the carrier's declared
4553
+ * commitment: the memo's commitment field is written by the relay and is not covered by the
4554
+ * envelope's Poly1305 tag, so a carrier can claim any commitment it likes. With this set, a
4555
+ * carrier survives only if `Poseidon(amount, ownerPubkey, blinding, mint)` reproduces it.
4556
+ */
4557
+ ownerUtxoPublicKey?: bigint;
4558
+ /** Candidate pool mints to try when verifying. Defaults to the native-SOL sentinel. */
4559
+ mints?: PublicKey[];
4560
+ onStatus?: (status: string) => void;
4561
+ debug?: boolean;
3379
4562
  }
3380
4563
  /**
3381
- * Scan on-chain transactions for the Cloak program, extract chain
3382
- * notes, trial-decrypt with the provided viewing key, verify integrity,
3383
- * and return a sorted list of the caller's transactions.
4564
+ * Sweep the recipient-delivery registry (CLKD1) and trial-open every carrier with the caller's
4565
+ * viewing key.
3384
4566
  *
3385
- * This is fully self-contained it only needs a Solana RPC connection
3386
- * and the user's viewing key (private).
4567
+ * This is the READ half of the fix for S2S-08 / VK-01. The pre-existing sweep
4568
+ * (`scanSwapNoteCarriers`) reads the CLK1 registry, which carries the SENDER-encrypted compliance
4569
+ * note — which is why a cold scan found the withdrawal change note and nothing that was sent TO the
4570
+ * scanning wallet. This registry carries the recipient-encrypted envelope, and is the only place a
4571
+ * recipient's own note is discoverable from `(rpc, programId, nk)` alone.
3387
4572
  */
4573
+ declare function scanRecipientDeliveryNotes(opts: ScanRecipientDeliveryOptions): Promise<{
4574
+ notes: DeliveredNote[];
4575
+ rpcCalls: number;
4576
+ }>;
3388
4577
  declare function scanTransactions(opts: ScanOptions): Promise<ScanResult>;
3389
4578
  /** JSON-serializable compliance report (numbers instead of bigint). Used for cache and display. */
3390
4579
  interface ComplianceReport {
@@ -3560,8 +4749,8 @@ declare class SimpleWallet {
3560
4749
  * @packageDocumentation
3561
4750
  */
3562
4751
 
3563
- declare const VERSION = "0.1.5";
4752
+ declare const VERSION = "0.2.0";
3564
4753
  /** True when scanner supports TransactSwap (tag 1). Check this to verify the correct SDK bundle is loaded. */
3565
4754
  declare const SCANNER_SUPPORTS_TRANSACT_SWAP = true;
3566
4755
 
3567
- export { CLOAK_PROGRAM_ID, type ChainNoteTxType, type CircuitVerificationResult, type CloakConfig, CloakError, type CloakKeyPair, type CloakNote, CloakSDK, type CommitmentEntry, type CommitmentsResponse, type CompactChainNote, type ComplianceReport, type ComplianceTxType, DEFAULT_CIRCUITS_URL, DEFAULT_TRANSACTION_CIRCUITS_URL, type DepositInstructionParams, type DepositOptions, type DepositResult, type DepositStatus, EXPECTED_CIRCUIT_HASHES, type EncryptedMetadataBundle, type EncryptedNote$1 as EncryptedNote, type ErrorCategory, type ExpandedSpendKey, FIXED_FEE_LAMPORTS, type Groth16Proof, LAMPORTS_PER_SOL, LocalStorageAdapter, type LogLevel, type Logger, MERKLE_TREE_HEIGHT, MIN_DEPOSIT_LAMPORTS, type MasterKey, type MaxLengthArray, MemoryStorageAdapter, type MerkleProof, type MerkleRootResponse, MerkleTree, NATIVE_SOL_MINT, type Network, type NoteData, type OnchainMerkleProof, type PendingDeposit, type PendingWithdrawal, type ProofResult, RelayInternalError, RelayService, type RiskQuoteInstructionResponse, RootNotFoundError, SCANNER_SUPPORTS_TRANSACT_SWAP, SIGN_IN_MESSAGE, SanctionsQuoteError, type ScanOptions, type ScanResult, type ScanSummary, type ScannedTransaction, ShieldPoolErrors, type ShieldPoolPDAs, SimpleWallet, type SpendKey, type StorageAdapter, type SwapOptions, type SwapParams, type SwapResult, type TransactOptions, type TransactParams, type TransactResult, type TransactionMetadata, type Transfer, type TransferOptions, type TransferResult, type TxStatus, type UserFriendlyError, type Utxo, UtxoAlreadySpentError, type EncryptedNote as UtxoEncryptedNote, type UtxoKeypair, type UtxoSwapParams, type UtxoSwapResult, UtxoWallet, VARIABLE_FEE_DENOMINATOR, VARIABLE_FEE_NUMERATOR, VARIABLE_FEE_RATE, VERSION, type VerifyUtxosResult, type ViewKey, type ViewingKeyPair, type WalletAdapter, type WalletUtxo, type WithdrawOptions, type WithdrawRegularInputs, type WithdrawSubmissionResult, type WithdrawSwapInputs, areCircuitsAvailable, bigintToBytes32$1 as bigintToBytes32, bigintToHex, buildMerkleTree, buildMerkleTreeFromChain, buildMerkleTreeFromRelay, buildPublicInputsBytes, bytesToHex, calculateFee, calculateFeeBigint, calculateRelayFee, chainNoteFromBase64, chainNoteToBase64, classifyRelayError, cleanupStalePendingOperations, clearPendingDeposits, clearPendingWithdrawals, computeChainNoteHash, computeCommitment$1 as computeCommitment, computeExtDataHash, computeMerkleRoot, computeNullifier$1 as computeNullifier, computeNullifierAsync, computeNullifierSync, computeOutputsHash, computeOutputsHashAsync, computeOutputsHashSync, computeProofForLatestDeposit, computeProofFromChain, computeSignature, computeSwapOutputsHash, computeSwapOutputsHashAsync, computeSwapOutputsHashSync, computeCommitment as computeUtxoCommitment, computeNullifier as computeUtxoNullifier, copyNoteToClipboard, createCloakError, createDepositInstruction, createLogger, createUtxo, createZeroUtxo, decryptCompactChainNote, decryptComplianceMetadataWithMasterKey, decryptTransactionMetadata, deriveDiversifiedViewingKey, deriveDiversifier, derivePublicKey, deriveSpendKey, deriveUserCompliancePublicKey, deriveUserComplianceScalar, deriveUtxoKeypairFromSpendKey, deriveViewKey, deriveViewingKeyFromNk, deriveViewingKeyFromSpendKey, deriveViewingKeyFromUtxoPrivateKey, deserializeUtxo, detectNetworkFromRpcUrl, downloadNote, encodeNoteSimple, encryptCompactChainNote, encryptNoteForRecipient, encryptTransactionMetadata, encryptTransactionMetadataBundle, expandSpendKey, exportKeys, exportNote, exportWalletKeys, fetchCommitments, fetchRiskQuoteInstruction, fetchRiskQuoteIx, filterNotesByNetwork, filterWithdrawableNotes, findNoteByCommitment, formatAmount, formatComplianceCsv, formatErrorForLogging, formatSol, fullWithdraw, generateCloakKeys, generateCommitment, generateCommitmentAsync, generateMasterSeed, generateNote, generateNoteFromWallet, generateUtxoKeypair, generateViewingKeyPair, generateWithdrawRegularProof, generateWithdrawSwapProof, getAddressExplorerUrl, getCircuitsPath, getDefaultCircuitsPath, getDistributableAmount, getExplorerUrl, getNkFromUtxoPrivateKey, getNullifierPDA, getPendingOperationsSummary, getPublicKey, getPublicViewKey, getRecipientAmount, getRpcUrlForNetwork, getShieldPoolPDAs, getSwapStatePDA, getViewKey, hasPendingOperations, hexToBigint$1 as hexToBigint, hexToBytes, importKeys, importWalletKeys, isDebugEnabled, isRootNotFoundError, isValidHex, isValidRpcUrl, isValidSolanaAddress, isWithdrawAmountSufficient, isWithdrawable, keypairToAdapter, loadPendingDeposits, loadPendingWithdrawals, parseAmount, parseError, parseNote, parseRelayErrorResponse, parseTransactionError, partialWithdraw, poseidonHash, preflightCheck, preflightNullifiers, prepareEncryptedOutput, prepareEncryptedOutputForRecipient, proofToBytes, pubkeyToFieldElement, pubkeyToLimbs, randomBytes, randomFieldElement, readMerkleTreeState, registerViewingKey, removePendingDeposit, removePendingWithdrawal, savePendingDeposit, savePendingWithdrawal, scanNotesForWallet, scanTransactions, sdkLogger, selectUtxos, sendTransaction, serializeNote, serializeUtxo, setCircuitsPath, setDebugMode, signTransaction, splitTo2Limbs, sumUtxoAmounts, swapUtxo, swapWithChange, toComplianceReport, transact, transfer, truncate, tryDecryptNote, updateNoteWithDeposit, updatePendingDeposit, updatePendingWithdrawal, bigintToBytes32 as utxoBigintToBytes32, utxoEquals, hexToBigint as utxoHexToBigint, validateDepositParams, validateNote, validateOutputsSum, validateRoot, validateTransfers, validateWalletConnected, validateWithdrawableNote, verifyAllCircuits, verifyCircuitIntegrity, verifyUtxos, waitForRoot, withTiming };
4756
+ export { type BuildRecipientDeliveryNotesParams, CHAIN_NOTE_SALT_BITS, CLOAK_PROGRAM_ID, type ChainNoteTxType, type CircuitName, type CircuitVerificationResult, type ClaimTimeoutRefundOptions, type ClaimTimeoutRefundParams, type ClaimTimeoutRefundResult, type CloakConfig, CloakError, type CloakKeyPair, type CloakNote, CloakSDK, type CommitmentEntry, type CommitmentsResponse, type CompactChainNote, type ComplianceReport, type ComplianceTxType, type ConfirmSettlementParams, DEFAULT_CIRCUITS_URL, DEFAULT_TRANSACTION_CIRCUITS_URL, DELIVERY_MEMO_TAG, DELIVERY_REGISTRY_SEED, type DeliveredNote, type DepositInstructionParams, type DepositNoteSecrets, type DepositOptions, type DepositResult, type DepositStatus, type DiscoverSwapRefundsOptions, type DiscoveredSwapRefund, EXPECTED_CIRCUIT_HASHES, type EncryptedMetadataBundle, type EncryptedNote$1 as EncryptedNote, type ErrorCategory, type ExpandedSpendKey, FIXED_FEE_LAMPORTS, type Groth16Proof, InsecureRandomnessError, LAMPORTS_PER_SOL, LocalStorageAdapter, type LogLevel, type Logger, MERKLE_TREE_HEIGHT, MIN_DEPOSIT_LAMPORTS, type MasterKey, type MatchDepositNoteParams, type MatchSwapRefundLeafParams, type MaxLengthArray, MemoryStorageAdapter, type MerkleProof, type MerkleRootResponse, MerkleTree, NATIVE_SOL_MINT, type Network, type NoteData, type OnchainMerkleProof, type ParsedDeliveryCarrier, type PendingDeposit, type PendingWithdrawal, type ProofResult, RECIPIENT_DELIVERY_CIPHERTEXT_LEN, RECIPIENT_DELIVERY_EPHEMERAL_PK_LEN, RECIPIENT_DELIVERY_NONCE_LEN, RECIPIENT_DELIVERY_NOTE_BYTES, RECIPIENT_DELIVERY_PLAINTEXT_LEN, RECIPIENT_DELIVERY_TAG_LEN, type RecipientDeliveryNote, type RecoveredDepositNote, type RecoveredDepositNoteRecord, type RecoveredSwapRefund, type RefundVoucher, RelayInternalError, RelayService, type RelaySubmissionResult, type RiskQuoteInstructionResponse, RootNotFoundError, SCANNER_SUPPORTS_TRANSACT_SWAP, SIGN_IN_MESSAGE, SanctionsQuoteError, type ScanOptions, type ScanRecipientDeliveryOptions, type ScanResult, type ScanSummary, type ScannedTransaction, type SettlementConnection, type SettlementContext, type SettlementStatus, type SettlementVerdict, SettlementVerificationError, ShieldPoolErrors, type ShieldPoolPDAs, SimpleWallet, type SpendKey, type StorageAdapter, type SubmitTransactToRelayArgs, type SwapOptions, type SwapParams, type SwapRefundAuthorization, type SwapResult, TRANSACTION_CIRCUITS_VERSION, type TransactOptions, type TransactParams, type TransactRequestBodyParams, type TransactResult, type TransactionMetadata, type Transfer, type TransferOptions, type TransferResult, type TxStatus, type UserFriendlyError, type Utxo, UtxoAlreadySpentError, type EncryptedNote as UtxoEncryptedNote, type UtxoKeypair, type UtxoSwapParams, type UtxoSwapResult, UtxoWallet, VARIABLE_FEE_DENOMINATOR, VARIABLE_FEE_NUMERATOR, VARIABLE_FEE_RATE, VERSION, type VerifyUtxosResult, type ViewKey, type ViewingKeyPair, type WalletAdapter, type WalletUtxo, type WithdrawOptions, type WithdrawRegularInputs, type WithdrawSubmissionResult, type WithdrawSwapInputs, areCircuitsAvailable, assertDirectSubmissionLanded, assertTransactionCircuitIntegrity, bigintToBytes32$1 as bigintToBytes32, bigintToHex, buildMerkleTree, buildMerkleTreeFromChain, buildMerkleTreeFromRelay, buildPublicInputsBytes, buildRecipientDeliveryNotes, buildTransactRequestBody, bytesToHex, calculateFee, calculateFeeBigint, calculateRelayFee, canRebuildMerkleTreeFromChain, chainNoteFromBase64, chainNoteToBase64, claimTimeoutRefund, classifyRelayError, cleanupStalePendingOperations, clearPendingDeposits, clearPendingWithdrawals, computeChainNoteHash, computeCommitment$1 as computeCommitment, computeExtDataHash, computeMerkleRoot, computeNullifier$1 as computeNullifier, computeNullifierAsync, computeNullifierSync, computeOutputsHash, computeOutputsHashAsync, computeOutputsHashSync, computeProofForLatestDeposit, computeProofFromChain, computeSignature, computeSwapOutputsHash, computeSwapOutputsHashAsync, computeSwapOutputsHashSync, computeSwapRefundCommitment, computeCommitment as computeUtxoCommitment, computeNullifier as computeUtxoNullifier, confirmTransactSettlement, copyNoteToClipboard, createCloakError, createDepositInstruction, createLogger, createRecoverableDepositUtxo, createUtxo, createZeroUtxo, decryptCompactChainNote, decryptComplianceMetadataWithMasterKey, decryptTransactionMetadata, deriveDepositNoteSecrets, deriveDiversifiedViewingKey, deriveDiversifier, deriveInputNullifierPdas, derivePublicKey, deriveSpendKey, deriveSwapRefundAuthorization, deriveUserCompliancePublicKey, deriveUserComplianceScalar, deriveUtxoKeypairFromSpendKey, deriveViewKey, deriveViewingKeyFromNk, deriveViewingKeyFromSpendKey, deriveViewingKeyFromUtxoPrivateKey, deserializeUtxo, detectNetworkFromRpcUrl, discoverSwapRefunds, downloadNote, encodeDeliveryCarrierMemo, encodeNoteSimple, encodeRecipientDeliveryNote, encryptCompactChainNote, encryptNoteForRecipient, encryptTransactionMetadata, encryptTransactionMetadataBundle, expandSpendKey, exportKeys, exportNote, exportWalletKeys, fetchCommitments, fetchRiskQuoteInstruction, fetchRiskQuoteIx, filterNotesByNetwork, filterWithdrawableNotes, findNoteByCommitment, formatAmount, formatComplianceCsv, formatErrorForLogging, formatSol, fullWithdraw, generateCloakKeys, generateCommitment, generateCommitmentAsync, generateMasterSeed, generateNote, generateNoteFromWallet, generateUtxoKeypair, generateViewingKeyPair, generateWithdrawRegularProof, generateWithdrawSwapProof, getAddressExplorerUrl, getChainNoteRegistryPDA, getCircuitsPath, getDefaultCircuitsPath, getDeliveryRegistryPDA, getDistributableAmount, getExplorerUrl, getNkFromUtxoPrivateKey, getNullifierPDA, getPendingOperationsSummary, getPoolAuthorityConfigPDA, getPublicKey, getPublicViewKey, getRecipientAmount, getRefundClaimPDA, getRefundLedgerPDA, getRpcUrlForNetwork, getShieldPoolPDAs, getSwapStatePDA, getViewKey, hasPendingOperations, hexToBigint$1 as hexToBigint, hexToBytes, importKeys, importWalletKeys, isBrowser, isBrowserLike, isDebugEnabled, isPlausibleSignature, isReactNative, isRootNotFoundError, isSubmissionOutcomeUnknownResponse, isValidHex, isValidRpcUrl, isValidSolanaAddress, isWithdrawAmountSufficient, isWithdrawable, keypairToAdapter, loadPendingDeposits, loadPendingWithdrawals, loadVerifiedCircuitArtifacts, matchDepositNote, matchSwapRefundLeaf, openRecipientDeliveryNote, parseAmount, parseDeliveryCarrierMemo, parseError, parseNote, parseRelayErrorResponse, parseRelayErrorSignature, parseTransactionError, partialWithdraw, poseidonHash, preflightCheck, preflightNullifiers, prepareEncryptedOutput, prepareEncryptedOutputForRecipient, proofToBytes, pubkeyToFieldElement, pubkeyToLimbs, randomBytes, randomDepositNoteSalt, randomFieldElement, readMerkleTreeState, recipientDeliveryNoteToBase64, registerViewingKey, removePendingDeposit, removePendingWithdrawal, resolveCircuitsBase, savePendingDeposit, savePendingWithdrawal, scanNotesForWallet, scanRecipientDeliveryNotes, scanTransactions, sdkLogger, selectUtxos, sendTransaction, serializeNote, serializeUtxo, setCircuitsPath, setDebugMode, signTransaction, splitTo2Limbs, submitTransactToRelay, sumUtxoAmounts, swapUtxo, swapWithChange, toComplianceReport, transact, transfer, truncate, tryDecryptNote, updateNoteWithDeposit, updatePendingDeposit, updatePendingWithdrawal, bigintToBytes32 as utxoBigintToBytes32, utxoEquals, hexToBigint as utxoHexToBigint, validateDepositParams, validateNote, validateOutputsSum, validateRoot, validateTransfers, validateWalletConnected, validateWithdrawableNote, verifyAllCircuits, verifyCircuitIntegrity, verifyUtxos, waitForRoot, withTiming };