@cloak.dev/sdk
v0.1.6
Published
Shield, send, and swap on Solana privately — TypeScript SDK with UTXO-based zero-knowledge transactions
Maintainers
Readme
@cloak.dev/sdk
TypeScript SDK for the Cloak Protocol - Private transactions on Solana using zero-knowledge proofs.
Features
- 🔒 Private Transfers: Shield, send, and reclaim SOL or SPL tokens via the UTXO model and zero-knowledge proofs
- 🔁 Shield-to-shield: Move funds privately between UTXOs (
transfer) without ever de-shielding - 💱 Token Swaps: Swap shielded SOL for SPL tokens via Jupiter (
swapUtxo) — SOL → USDC, BRZ, etc. - 🔐 Type-Safe: Full TypeScript types; the
getConfig()snapshot is structurally narrower than the constructor input so secrets cannot be re-exposed by accident - 🌐 Cross-Platform: Browser (wallet adapter) and Node.js (
Keypair) modes - ⚡ Functional API:
transact/transfer/partialWithdraw/fullWithdraw/swapUtxo— pass connection + program + relay + signer per call
Installation
npm install @cloak.dev/sdk @solana/web3.js
# or
yarn add @cloak.dev/sdk @solana/web3.js
# or
pnpm add @cloak.dev/sdk @solana/web3.jsNote: For swap functionality, you'll also need @solana/spl-token:
npm install @solana/spl-tokenQuick Start
SDK Defaults (Recommended)
- Standard integrations should use SDK defaults for program, relay, and circuits.
- Do not expose protocol-level config (
programId, relay URL, circuits URL) as end-user input. transact,partialWithdraw, andfullWithdrawalready include stale-root retry handling.- For simple CLI sends, require only
SOLANA_RPC_URLandKEYPAIR_PATH.
Minimal Private SOL Send (Single File, Keypair)
Use this contract for one-shot scripts and AI-generated snippets.
import { readFileSync } from "fs";
import {
CLOAK_PROGRAM_ID,
NATIVE_SOL_MINT,
createUtxo,
createZeroUtxo,
fullWithdraw,
generateUtxoKeypair,
transact,
} from "@cloak.dev/sdk";
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
async function main() {
const [recipientArg, lamportsArg] = process.argv.slice(2);
if (!recipientArg || !lamportsArg) {
throw new Error("Usage: npx tsx send-sol-private.ts <recipientPubkey> <lamports>");
}
const rpcUrl = process.env.SOLANA_RPC_URL;
const keypairPath = process.env.KEYPAIR_PATH;
if (!rpcUrl || !keypairPath) {
throw new Error("Set SOLANA_RPC_URL and KEYPAIR_PATH");
}
const connection = new Connection(rpcUrl, "confirmed");
const signer = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(readFileSync(keypairPath, "utf8"))),
);
const recipient = new PublicKey(recipientArg);
const amountLamports = BigInt(lamportsArg);
const owner = await generateUtxoKeypair();
const output = await createUtxo(amountLamports, owner, NATIVE_SOL_MINT);
const deposited = await transact(
{
inputUtxos: [await createZeroUtxo(NATIVE_SOL_MINT)],
outputUtxos: [output],
externalAmount: amountLamports,
depositor: signer.publicKey,
},
{
connection,
programId: CLOAK_PROGRAM_ID,
depositorKeypair: signer,
walletPublicKey: signer.publicKey,
enforceViewingKeyRegistration: false,
},
);
const withdrawn = await fullWithdraw(deposited.outputUtxos, recipient, {
connection,
programId: CLOAK_PROGRAM_ID,
depositorKeypair: signer,
walletPublicKey: signer.publicKey,
cachedMerkleTree: deposited.merkleTree,
enforceViewingKeyRegistration: false,
});
console.log(withdrawn.signature);
}
main().catch((e) => {
console.error(e instanceof Error ? e.message : String(e));
process.exit(1);
});Run:
SOLANA_RPC_URL="https://api.mainnet-beta.solana.com" \
KEYPAIR_PATH="/absolute/path/to/id.json" \
npx tsx send-sol-private.ts <recipientPubkey> <lamports>Hard rules for minimal scripts:
- Use lamports from CLI (
<lamports>) and keep transaction math inbigint. - Use
KEYPAIR_PATH; do not ask for raw private key env vars. - Do not parse SOL decimals with float math (
parseFloat,AMOUNT_SOL). - Keep
programIdfixed toCLOAK_PROGRAM_ID(no end-user override).
Maintained Examples
npm run example:fast-send
npm run example:fast-usdc-send
npm run example:usdc-pool-transfer
npm run example:swap
npm run example:swap-recovery
npm run example:swap-usdc
npm run example:swap-brz
npm run example:transfer
npm run test:examplesexample:transfer runs deposit -> shield-to-shield transfer (public_amount=0) -> recipient withdraw verification and prints stable FULL_SIG|transfer|deposit|..., FULL_SIG|transfer|tx|..., and COMMITMENT_INDICES|transfer|[...] markers.
example:fast-usdc-send is the one-shot private send path for USDC recipients (deposit SOL, swap privately to USDC, deliver to recipient ATA).
example:usdc-pool-transfer is the mint-scoped USDC pool transfer path (User A deposits USDC into Cloak USDC pool, sends shielded USDC to User B, and User B spends the received shielded note).
example:swap-usdc is the canonical Nora swap evidence path (SOL -> USDC) and prints quote/route details, FULL_SIG|swap-usdc|transact_swap|..., FULL_SIG|swap-usdc|swap_completed|..., and RECIPIENT_USDC_BALANCE|... markers.
example:swap-recovery focuses on pending/timeout behavior. It submits a swap, tracks swap_phase + slots_remaining from relay /status, and can optionally call close_timed_out (AUTO_CLOSE_TIMED_OUT=1) once can_recover=true.
example:swap-brz first attempts BRZ (FtgGSFADXBtroxq8VCausXRr2of47QBf5AS1NtZCu4GD) and automatically falls back to USDC if BRZ routing is unavailable. It always emits QUOTE_ROUTE|..., SWAP_OUTPUT_MINT|..., and (when fallback happens) BRZ_FALLBACK_TO|....
test:examples runs all maintained examples in CLOAK_EXAMPLE_DRY_RUN=1 mode so CI/local checks validate script wiring without requiring funded wallets or live RPC execution.
Node.js (with Keypair)
import {
CLOAK_PROGRAM_ID,
NATIVE_SOL_MINT,
createUtxo,
createZeroUtxo,
fullWithdraw,
generateUtxoKeypair,
transact,
} from "@cloak.dev/sdk";
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
const connection = new Connection("https://api.mainnet-beta.solana.com");
const signer = Keypair.fromSecretKey(/* your secret key */);
// Generate a UTXO keypair (owner of the shielded balance)
const owner = await generateUtxoKeypair();
// Build a 0.01 SOL output UTXO and shield it
const amountLamports = 10_000_000n;
const output = await createUtxo(amountLamports, owner, NATIVE_SOL_MINT);
const deposited = await transact(
{
inputUtxos: [await createZeroUtxo(NATIVE_SOL_MINT), await createZeroUtxo(NATIVE_SOL_MINT)],
outputUtxos: [output, await createZeroUtxo(NATIVE_SOL_MINT)],
externalAmount: amountLamports, // positive = deposit
depositor: signer.publicKey,
},
{
connection,
programId: CLOAK_PROGRAM_ID,
relayUrl: "https://api.cloak.ag",
depositorKeypair: signer,
},
);
console.log("Deposit signature:", deposited.signature);
// Reclaim the full balance to a public address
const recipient = new PublicKey("RECIPIENT_ADDRESS");
const withdrawn = await fullWithdraw(deposited.outputUtxos, recipient, {
connection,
programId: CLOAK_PROGRAM_ID,
relayUrl: "https://api.cloak.ag",
depositorKeypair: signer,
cachedMerkleTree: deposited.merkleTree,
});
console.log("Withdraw signature:", withdrawn.signature);React/Next.js (with Wallet Adapter)
import {
CLOAK_PROGRAM_ID,
NATIVE_SOL_MINT,
createUtxo,
createZeroUtxo,
generateUtxoKeypair,
transact,
} from "@cloak.dev/sdk";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { useCallback } from "react";
export function DepositButton() {
const { connection } = useConnection();
const wallet = useWallet();
const onClick = useCallback(async () => {
if (!wallet.publicKey || !wallet.signTransaction) return;
const owner = await generateUtxoKeypair();
const amount = 10_000_000n; // 0.01 SOL — SDK-enforced minimum
const output = await createUtxo(amount, owner, NATIVE_SOL_MINT);
const result = await transact(
{
inputUtxos: [await createZeroUtxo(NATIVE_SOL_MINT), await createZeroUtxo(NATIVE_SOL_MINT)],
outputUtxos: [output, await createZeroUtxo(NATIVE_SOL_MINT)],
externalAmount: amount,
depositor: wallet.publicKey,
},
{
connection,
programId: CLOAK_PROGRAM_ID,
relayUrl: "https://api.cloak.ag",
wallet: {
publicKey: wallet.publicKey,
signTransaction: wallet.signTransaction,
},
},
);
console.log("Deposited:", result.signature);
}, [connection, wallet]);
return <button onClick={onClick}>Deposit 0.01 SOL</button>;
}The CloakSDK class is kept as a thin config + read-only chain helper
(getPublicKey, getCurrentRoot, getMerkleProof, getTransactionStatus,
importWalletKeys, exportWalletKeys, getConfig). All transaction-emitting
calls use the standalone helpers above.
Core Functions
Deposit (transact with positive externalAmount)
const result = await transact(
{
inputUtxos: [await createZeroUtxo(), await createZeroUtxo()],
outputUtxos: [outputUtxo, await createZeroUtxo()],
externalAmount: 10_000_000n, // positive = deposit
depositor: signer.publicKey,
},
{ connection, programId: CLOAK_PROGRAM_ID, relayUrl: "https://api.cloak.ag", depositorKeypair: signer },
);
console.log("Leaf index:", result.commitmentIndices[0]);
console.log("Output UTXO:", result.outputUtxos[0]);Withdraw
import { fullWithdraw, partialWithdraw } from "@cloak.dev/sdk";
// Reclaim everything in one or more input UTXOs to a public address.
const result = await fullWithdraw(myUtxos, recipientPublicKey, {
connection,
programId: CLOAK_PROGRAM_ID,
relayUrl: "https://api.cloak.ag",
depositorKeypair: signer,
});
// Or withdraw a portion and keep the change shielded.
const partial = await partialWithdraw(myUtxos, recipientPublicKey, 5_000_000n, {
connection,
programId: CLOAK_PROGRAM_ID,
relayUrl: "https://api.cloak.ag",
depositorKeypair: signer,
});Shield-to-shield Transfer
import { transfer } from "@cloak.dev/sdk";
const result = await transfer(myUtxos, recipientUtxoPublicKey, amountLamports, {
connection,
programId: CLOAK_PROGRAM_ID,
relayUrl: "https://api.cloak.ag",
depositorKeypair: signer,
});Swap (SOL → SPL token)
import { swapUtxo } from "@cloak.dev/sdk";
const result = await swapUtxo(
{
inputUtxos: myUtxos,
swapAmount: 10_000_000n,
outputMint: new PublicKey("TOKEN_MINT_ADDRESS"),
recipientAta: recipientTokenAccount,
minOutputAmount: 1_000_000n,
},
{
connection,
programId: CLOAK_PROGRAM_ID,
relayUrl: "https://api.cloak.ag",
depositorKeypair: signer,
},
);Fee Structure
- Fixed Fee: 0.005 SOL (5,000,000 lamports) on withdraw
- Variable Fee: 0.3% of withdrawn amount
Use getDistributableAmount() to calculate the amount a recipient receives after fees:
import { getDistributableAmount } from "@cloak.dev/sdk";
const withdrawing = 100_000_000; // 0.1 SOL
const recipientReceives = getDistributableAmount(withdrawing); // ~94,700,000 lamportsUTXOs
A UTXO is a cryptographic commitment over (amount, ownerPubkey, blinding, mintAddress). Each transact call consumes up to 2 input UTXOs and produces up to 2 outputs:
interface Utxo {
amount: bigint; // lamports for SOL, token units for SPL
keypair: UtxoKeypair; // { privateKey, publicKey } — bigints
blinding: bigint; // randomness
mintAddress: PublicKey; // NATIVE_SOL_MINT for SOL
index?: number; // leaf index in the Merkle tree (set after the UTXO is in-tree)
commitment?: bigint;
nullifier?: bigint;
}⚠️ Important: persist the spend privateKey, blinding, amount, index, and mintAddress for every output UTXO — those are the only inputs you need to spend it later. The SDK ships LocalStorageAdapter for browser wallet keys; UTXO persistence is the consumer's responsibility (see lib/utxo-manager.ts patterns in cloak-ag/web for a reference implementation).
Compliance Chain Scanning
Cloak supports a viewing-key commitment flow for compliance and self-discovery:
- The viewing key itself is never written on-chain.
- Only
viewing_key_commitment = SHA256(viewing_key_public)is stored on-chain. - A scanner recomputes this commitment from
viewing_key_publicand matches program transactions.
sequenceDiagram
autonumber
participant U as User Wallet + SDK
participant R as Relay
participant C as Cloak Program (Solana)
participant S as Scanner
U->>U: Generate (vk_priv, vk_pub)
U->>U: vkc = SHA256(vk_pub)
U->>R: Register vk_priv (signed)
U->>R: POST /transact + metadata_bundle + viewing_key_commitment=vkc
R->>C: Submit instruction [proof|public_inputs|vkc]
C-->>R: Confirm tx signature
R-->>U: Return signature
S->>S: Compute target_vkc from vk_pub
S->>C: Fetch recent program txs
S->>S: Decode ix data, extract vkc bytes, filter target_vkc
alt Relay enrichment enabled
S->>R: POST /admin/compliance/decrypt (admin signed)
R-->>S: Decrypted metadata rows (amount/recipient/type)
S->>S: Join on commitment + public_amount hints
end
S-->>U: Matching transactionsError Handling
import {
CloakError,
UtxoAlreadySpentError,
RootNotFoundError,
classifyRelayError,
fullWithdraw,
CLOAK_PROGRAM_ID,
} from "@cloak.dev/sdk";
try {
await fullWithdraw(myUtxos, recipient, {
connection,
programId: CLOAK_PROGRAM_ID,
relayUrl: "https://api.cloak.ag",
depositorKeypair: signer,
});
} catch (error) {
if (error instanceof UtxoAlreadySpentError) {
// Drop the spent UTXO from local storage; the structured error
// carries which nullifiers collided.
console.log("Spent nullifiers:", error.nullifiers);
} else if (error instanceof RootNotFoundError) {
// Relay/chain root mismatch; retry against a fresh tree.
} else if (error instanceof CloakError) {
console.log("Category:", error.category); // 'wallet' | 'network' | 'prover' | 'relay' | 'validation' | 'environment' | 'service' | 'indexer'
console.log("Retryable:", error.retryable);
}
}Links
- Website: https://cloak.ag
- Documentation: https://docs.cloak.ag
- GitHub: https://github.com/cloak-ag/sdk
License
Apache-2.0
