Solana Integration
FHE-powered slot-based private token transfers on Solana.
Solana support is coming soon. The programs and SDK are being finalized. Check back soon or follow our updates!
Overview
Private Token Slots lets you build confidential token vaults on Solana where:
- Balances are encrypted (no one sees how much anyone holds)
- Transfers happen privately (sender, receiver, amount all hidden)
- Only the vault owner can decrypt balances
┌─────────────────────────────────────────────────────────┐
│ Your Token Vault │
├─────────┬─────────┬─────────┬─────────┬────────────────┤
│ Slot 0 │ Slot 1 │ Slot 2 │ Slot 3 │ ... Slot 255 │
│ [ENC] │ [ENC] │ [ENC] │ [ENC] │ [ENC] │
└─────────┴─────────┴─────────┴─────────┴────────────────┘
↓ Transfer 50 from Slot 0 → Slot 1
┌─────────┬─────────┬─────────┬─────────┬────────────────┐
│ [ENC] │ [ENC] │ [ENC] │ [ENC] │ [ENC] │
└─────────┴─────────┴─────────┴─────────┴────────────────┘
All balances remain encrypted throughout!
How It Works
Architecture
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Your Client │ │ Solana │ │ ZKSDK │
│ │ │ │ │ Coprocessor │
│ 1. Generate │ │ 3. Vault CPI │ │ │
│ FHE keys │────→│ to Oracle │────→│ 4. Compute │
│ │ │ │ │ on FHE │
│ 2. Encrypt │ │ │ │ │
│ transfer │ │ │ │ 5. Return │
│ │ │ │◄────│ result │
│ 6. Decrypt │◄────│ │ │ │
│ (only you)│ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
What happens at each step:
- Generate FHE keys — You create keys locally. Your secret key (
clientKey) never leaves your machine. - Encrypt transfer — You encrypt the transfer details (which slot, how much). No one sees the plaintext.
- Vault CPI — Your vault program sends the encrypted request to the coprocessor via Solana.
- Compute on FHE — The coprocessor does math on encrypted data. It updates balances without knowing what they are.
- Return result — New encrypted balances are written back to your vault on-chain.
- Decrypt — Only you can decrypt and see the actual balances.
Why Deploy Your Own Vault Program?
You deploy your own vault program (Solana program) because:
- Custom logic — Add your own deposit/withdrawal rules, access control, fees, etc.
- Your keys — You generate FHE keys locally, register with coprocessor, link to your vault
- Slots = accounts — Each slot (0-255) holds an encrypted balance within your vault
The coprocessor is a singleton — all vault programs CPI to the same coprocessor program. You don't deploy the coprocessor, just your vault.
How deposits work:
- User calls your vault's
deposit()→ tokens go to vault's token account - Your vault CPIs to coprocessor → coprocessor encrypts amount into a slot
- New balance blob is stored (currently in worker storage, future: IPFS/Arweave/user DB)
- Vault on-chain gets updated CID pointer — only you (with clientKey) can decrypt
Zero-Trust Security
Your keys never leave your machine:
| Component | Where It Lives | Who Can Access |
|---|---|---|
| ClientKey | Your machine only | Only you |
| ServerKey | ZKSDK Coprocessor | Can compute, cannot decrypt |
| PublicKey | ZKSDK Coprocessor | Anyone can encrypt to vault |
The coprocessor mathematically cannot decrypt your data. It only has evaluation keys that can perform operations on ciphertext.
Quick Start
Install the SDK
npm install @zksdk/js-private-token-slots-fhe @coral-xyz/anchor @solana/web3.js @solana/spl-token
Network Configuration
// Program IDs (Solana Devnet)
const YOUR_VAULT_PROGRAM_ID = new PublicKey("YOUR_DEPLOYED_VAULT_PROGRAM_ID");
const COPROCESSOR_PROGRAM_ID = new PublicKey(
"5LLZpQKc8DydeB6mnFU7SD6LRtNRBVRhzhUR3aNBbnmV"
);
// ZKSDK Alpha Network
const WORKER_URL = "https://alpha.coprocessor.zksdk.com";
const RPC_URL = "https://api.devnet.solana.com";
The on-chain program uses "oracle" naming (oracleProgram, oracleRequest). This is legacy terminology — conceptually it's a coprocessor that performs FHE computation.
Step-by-Step Guide
Understanding the Two IDs
Before starting, understand there are two separate IDs:
| ID | What It Is | Where It Lives |
|---|---|---|
| Vault Key ID | 64-char hex ID for your FHE keys | FHE Worker (coprocessor) |
| Vault Program ID | Your deployed Solana program | Solana blockchain |
1. Generate FHE Keys & Register with Coprocessor
First, generate FHE keys locally and register them with the coprocessor. This gives you a vaultKeyId.
import { SlotVaultClient } from "@zksdk/js-private-token-slots-fhe";
const client = new SlotVaultClient({
workerUrl: "https://alpha.coprocessor.zksdk.com",
});
// Generate FHE keys locally (ZERO TRUST!)
const { vaultKeyId, clientKey, serverKey, publicKey } =
await client.createVault();
// ⚠️ CRITICAL: Save clientKey securely - only YOU can decrypt!
console.log("Vault Key ID:", vaultKeyId);
// Example: "a1b2c3d4e5f6...64 chars..."
What happens:
- Keys generated on YOUR machine (clientKey never sent anywhere)
- ServerKey + PublicKey sent to coprocessor
- Coprocessor returns
vaultKeyId(64-char hex)
2. Deploy Your Vault Program
Now deploy your Solana program. The reference vault has the ZKSDK already integrated.
Option A: Clone and deploy the reference vault
git clone https://github.com/zksdk-labs/example-solana-fhe-slot-vault
cd example-solana-fhe-slot-vault
anchor build
anchor deploy --provider.cluster devnet
After deploy, note your program ID:
solana address -k target/deploy/token_vault-keypair.json
# Example: "YourVau1tPr0gramID..."
Option B: Add SDK to your existing program
# Cargo.toml
[dependencies]
zksdk-private-token-slots = "0.1.0"
The SDK gives you the CPI functions to call the coprocessor.
3. Initialize Vault Account On-Chain
Now link your on-chain vault to your FHE keys using the vaultKeyId from Step 1.
// Derive vault PDA
const [vaultPda] = PublicKey.findProgramAddressSync(
[Buffer.from("vault"), tokenMint.toBuffer()],
YOUR_VAULT_PROGRAM_ID
);
// Initialize vault with your FHE vault key ID
await vaultProgram.methods
.initializeVault(vaultKeyId)
.accounts({
vault: vaultPda,
tokenMint: tokenMint,
vaultTokenAccount: vaultTokenAccount,
authority: wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.rpc();
4. Deposit Tokens
// Derive user position PDA
const [userPositionPda] = PublicKey.findProgramAddressSync(
[Buffer.from("position"), vaultPda.toBuffer(), wallet.publicKey.toBuffer()],
YOUR_VAULT_PROGRAM_ID
);
// Deposit 100 tokens (6 decimals)
await vaultProgram.methods
.deposit(new anchor.BN(100_000_000))
.accounts({
vault: vaultPda,
userPosition: userPositionPda,
userTokenAccount: userTokenAccount,
vaultTokenAccount: vaultTokenAccount,
oracleProgram: COPROCESSOR_PROGRAM_ID, // Legacy name - this is the coprocessor
user: wallet.publicKey,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
})
.rpc();
// Wait for coprocessor to create balance blob
let balanceBlobCid = "";
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 2000));
const vault = await vaultProgram.account.vault.fetch(vaultPda);
if (vault.balanceBlobCid) {
balanceBlobCid = vault.balanceBlobCid;
break;
}
}
5. Private Transfer
// Encrypt transfer: Slot 0 → Slot 1, Amount 50
const { encrypted_data } = client.encryptTransfer(0, 1, 50);
// Upload to coprocessor storage
const cid = await client.storeBalanceBlob(encrypted_data);
// Submit to Solana
const oracleRequestKeypair = Keypair.generate();
await vaultProgram.methods
.requestPrivateTransfer(`c=${cid}`, 0, Buffer.from([]))
.accounts({
vault: vaultPda,
userPosition: userPositionPda,
oracleRequest: oracleRequestKeypair.publicKey,
oracleProgram: COPROCESSOR_PROGRAM_ID, // Legacy name - this is the coprocessor
user: wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([oracleRequestKeypair])
.rpc();
6. Wait & Decrypt
// Poll for completion
let resultCid = null;
for (let i = 0; i < 45; i++) {
await new Promise((r) => setTimeout(r, 2000));
const account = await oracleProgram.account.oracleRequest.fetch(
oracleRequestKeypair.publicKey
);
const status = Object.keys(account.status)[0];
if (status === "completed") {
resultCid = account.resultCid;
break;
}
}
// Fetch and decrypt (only YOU can!)
const encryptedResult = await client.fetchBalanceBlob(resultCid);
const result = client.decryptResult(encryptedResult);
console.log("New balances:", result.balances); // [205, 50, 0, 0, ...]
console.log("Transfer valid:", result.isValid); // true
Rust Integration
For Solana programs that need to CPI to the coprocessor:
# Cargo.toml
[dependencies]
zksdk-private-token-slots = "0.1.0"
use zksdk_core::prelude::*;
pub fn request_transfer(ctx: Context<RequestTransfer>, input_cid: String) -> Result<()> {
let metadata = VaultMetadataBuilder::new(
&ctx.accounts.vault.key(),
&ctx.accounts.vault.vault_key_id,
&ctx.accounts.vault.balance_blob_cid,
).build();
// Legacy function name - calls the coprocessor
submit_oracle_request(
&ctx.accounts.oracle_program,
&ctx.accounts.oracle_request,
&ctx.accounts.user,
&ctx.accounts.system_program,
CircuitType::CmuxTokenV1Linear.as_str(),
vec![],
&input_cid,
&metadata,
)?;
Ok(())
}
Network Configuration
ZKSDK Alpha Network (Testnet)
| Resource | Value |
|---|---|
| FHE Worker | https://alpha.coprocessor.zksdk.com |
| Coprocessor Program | 5LLZpQKc8DydeB6mnFU7SD6LRtNRBVRhzhUR3aNBbnmV |
| Reference Vault | BsVtbakPuEKJfsyFDXtCjWgHSxNbSXy1U4VPwsErSTDM (example only) |
| Chain | Solana Devnet |
| RPC URL | https://api.devnet.solana.com |
The Reference Vault above is just an example. You deploy your own vault program — the coprocessor listens to all vault programs that CPI to it.
Packages
| Package | Platform | Version |
|---|---|---|
| @zksdk/core | npm | 0.1.0 |
| @zksdk/js-private-token-slots-fhe | npm | 0.1.0 |
| zksdk-core | crates.io | 0.1.0 |
| zksdk-private-token-slots | crates.io | 0.1.0 |
API Reference
SlotVaultClient
import { SlotVaultClient } from '@zksdk/js-private-token-slots-fhe';
const client = new SlotVaultClient({
workerUrl: string,
apiKey?: string
});
// Create new vault (generates keys locally)
await client.createVault(): Promise<{
vaultKeyId: string; // 64-char hex ID
clientKey: string; // KEEP SECRET - for decryption
serverKey: string; // Safe to send - for computation
publicKey: string; // Safe to send - for encryption
}>;
// Load existing vault
await client.loadVault(vaultKeyId: string, clientKey?: string): Promise<VaultParams>;
// Encrypt transfer parameters
client.encryptTransfer(senderIdx: number, receiverIdx: number, amount: number): {
encrypted_data: string;
vault_key_id: string;
};
// Store encrypted blob
await client.storeBalanceBlob(encryptedBlob: string): Promise<string>; // Returns CID
// Fetch encrypted blob
await client.fetchBalanceBlob(cid: string): Promise<string>;
// Decrypt result (requires ClientKey)
client.decryptResult(encryptedResultB64: string): {
balances: number[]; // [205, 50, 0, 0, ...]
isValid: boolean; // Transfer validation result
};
Limitations (Alpha)
- Slot balances: 0-255 per slot (8-bit)
- Slots per vault: Up to 256
- Chain support: Solana devnet only
- Not audited: Do not use with real value
Storage
Encrypted data is stored off-chain. Configure storage in your SDK setup:
const client = new SlotVaultClient({
workerUrl: "https://alpha.coprocessor.zksdk.com",
storageType: "postgres", // Current default
});
| Type | Status |
|---|---|
postgres | Available |
celestia, avail, eigenda, ipfs | Coming Soon |
See full Storage documentation for all options and configuration details.
Full End-to-End Example
A complete example using the published SDK.
-
Vault Key ID — 64-char hex from
createVault()(Step 1)- This registers your FHE keys with the coprocessor
- You pass this to
initializeVault()on-chain
-
Your Vault Program — You deploy this on Solana (Step 2)
- Use the reference vault OR build your own with the SDK
- Your program CPIs to the singleton coprocessor
The coprocessor listens to ALL vault programs — you just deploy your vault, not the coprocessor.
import { SlotVaultClient } from "@zksdk/js-private-token-slots-fhe";
import { Connection, Keypair, PublicKey, SystemProgram } from "@solana/web3.js";
import * as anchor from "@coral-xyz/anchor";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
/**
* What talks to what:
*
* SDK (SlotVaultClient) ──HTTP──→ WORKER_URL (FHE coprocessor API)
* - createVault(), storeBalanceBlob(), fetchBalanceBlob()
* - encryptTransfer(), decryptResult() are local (no network)
*
* Your Vault Program ──CPI──→ COPROCESSOR_PROGRAM_ID (on-chain)
* - deposit(), requestPrivateTransfer()
* - The reference vault has this hardcoded already
*/
const WORKER_URL = "https://alpha.coprocessor.zksdk.com";
const RPC_URL = "https://api.devnet.solana.com";
const YOUR_VAULT_PROGRAM_ID = new PublicKey("YOUR_DEPLOYED_VAULT_PROGRAM_ID");
const COPROCESSOR_PROGRAM_ID = new PublicKey(
"5LLZpQKc8DydeB6mnFU7SD6LRtNRBVRhzhUR3aNBbnmV"
);
async function main() {
// ============ STEP 1: Generate FHE Keys & Register ============
// Keys generated locally, only serverKey + publicKey sent to coprocessor
const client = new SlotVaultClient({ workerUrl: WORKER_URL });
const { vaultKeyId, clientKey } = await client.createVault();
console.log("✅ Vault Key ID:", vaultKeyId);
// ⚠️ SAVE clientKey SECURELY - only YOU can decrypt!
// ============ STEP 2: Deploy Vault Program ============
// Already done - YOUR_VAULT_PROGRAM_ID is your deployed program
// See "Deploy Your Vault Program" section above
// ============ STEP 3: Initialize Vault On-Chain ============
// Links your on-chain vault to your FHE keys
const [vaultPda] = PublicKey.findProgramAddressSync(
[Buffer.from("vault"), tokenMint.toBuffer()],
YOUR_VAULT_PROGRAM_ID
);
await vaultProgram.methods
.initializeVault(vaultKeyId) // Pass the vault key ID from Step 1
.accounts({
vault: vaultPda,
tokenMint: tokenMint,
vaultTokenAccount: vaultTokenAccount,
authority: wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.rpc();
console.log("✅ Vault initialized on-chain");
// ============ STEP 4: Deposit Tokens ============
const [userPositionPda] = PublicKey.findProgramAddressSync(
[Buffer.from("position"), vaultPda.toBuffer(), wallet.publicKey.toBuffer()],
YOUR_VAULT_PROGRAM_ID
);
await vaultProgram.methods
.deposit(new anchor.BN(100_000_000)) // 100 tokens (6 decimals)
.accounts({
vault: vaultPda,
userPosition: userPositionPda,
userTokenAccount: userTokenAccount,
vaultTokenAccount: vaultTokenAccount,
oracleProgram: COPROCESSOR_PROGRAM_ID,
user: wallet.publicKey,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
})
.rpc();
console.log("✅ Tokens deposited");
// Wait for coprocessor to create balance blob
let balanceBlobCid = "";
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 2000));
const vault = await vaultProgram.account.vault.fetch(vaultPda);
if (vault.balanceBlobCid) {
balanceBlobCid = vault.balanceBlobCid;
break;
}
}
console.log("✅ Balance blob created:", balanceBlobCid);
// ============ STEP 5: Encrypt & Submit Transfer ============
const { encrypted_data } = client.encryptTransfer(0, 1, 50); // Slot 0 → Slot 1, 50 tokens
const cid = await client.storeBalanceBlob(encrypted_data);
console.log("✅ Encrypted transfer uploaded, CID:", cid);
const oracleRequestKeypair = Keypair.generate();
await vaultProgram.methods
.requestPrivateTransfer(`c=${cid}`, 0, Buffer.from([]))
.accounts({
vault: vaultPda,
userPosition: userPositionPda,
oracleRequest: oracleRequestKeypair.publicKey,
oracleProgram: COPROCESSOR_PROGRAM_ID,
user: wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([oracleRequestKeypair])
.rpc();
console.log("✅ Transfer request submitted");
// ============ STEP 6: Wait & Decrypt ============
let resultCid = null;
for (let i = 0; i < 45; i++) {
await new Promise((r) => setTimeout(r, 2000));
const account = await oracleProgram.account.oracleRequest.fetch(
oracleRequestKeypair.publicKey
);
if (Object.keys(account.status)[0] === "completed") {
resultCid = account.resultCid;
break;
}
}
const encryptedResult = await client.fetchBalanceBlob(resultCid!);
const result = client.decryptResult(encryptedResult);
console.log("✅ New balances:", result.balances); // [205, 50, 0, 0, ...]
console.log("✅ Transfer valid:", result.isValid);
}
main().catch(console.error);
The flow:
- Generate keys locally →
createVault()registers with coprocessor, returnsvaultKeyId - Deploy vault program → Your Solana program (already done)
- Initialize on-chain → Links vault PDA to your
vaultKeyId - Deposit tokens → Coprocessor creates encrypted balance blob
- Encrypt & submit → Encrypt transfer, upload, call vault
- Decrypt result → Only you can decrypt with your
clientKey
Run it:
npx ts-node your-e2e-test.ts
Support
- GitHub: example-solana-fhe-slot-vault
- Issues: Report bugs
- Contact: Get in touch