Skip to main content

Solana Integration

FHE-powered slot-based private token transfers on Solana.

V0.1 Alpha - Devnet Only

This is the first release of Private Token Slots. It is for testing purposes only — do not use with real value. The system is evolving rapidly and APIs may change.


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:

  1. Generate FHE keys — You create keys locally. Your secret key (clientKey) never leaves your machine.
  2. Encrypt transfer — You encrypt the transfer details (which slot, how much). No one sees the plaintext.
  3. Vault CPI — Your vault program sends the encrypted request to the coprocessor via Solana.
  4. Compute on FHE — The coprocessor does math on encrypted data. It updates balances without knowing what they are.
  5. Return result — New encrypted balances are written back to your vault on-chain.
  6. 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:

  1. User calls your vault's deposit() → tokens go to vault's token account
  2. Your vault CPIs to coprocessor → coprocessor encrypts amount into a slot
  3. New balance blob is stored (currently in worker storage, future: IPFS/Arweave/user DB)
  4. Vault on-chain gets updated CID pointer — only you (with clientKey) can decrypt

Zero-Trust Security

Your keys never leave your machine:

ComponentWhere It LivesWho Can Access
ClientKeyYour machine onlyOnly you
ServerKeyZKSDK CoprocessorCan compute, cannot decrypt
PublicKeyZKSDK CoprocessorAnyone 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/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';
Legacy Naming

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:

IDWhat It IsWhere It Lives
Vault Key ID64-char hex ID for your FHE keysFHE Worker (coprocessor)
Vault Program IDYour deployed Solana programSolana 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/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/zksdk
cd zksdk/chain-adapters/solana/token_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)

ResourceValue
FHE Workerhttps://alpha.coprocessor.zksdk.com
Coprocessor Program5LLZpQKc8DydeB6mnFU7SD6LRtNRBVRhzhUR3aNBbnmV
Reference VaultBsVtbakPuEKJfsyFDXtCjWgHSxNbSXy1U4VPwsErSTDM (example only)
ChainSolana Devnet
RPC URLhttps://api.devnet.solana.com
Deploy Your Own Vault

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

PackagePlatformVersion
@zksdk/corenpm0.1.0
@zksdk/private-token-slots-fhenpm0.1.0
zksdk-corecrates.io0.1.0
zksdk-private-token-slotscrates.io0.1.0

API Reference

SlotVaultClient

import { SlotVaultClient } from '@zksdk/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)

Current Limitations
  • Slot balances: 0-255 per slot (8-bit)
  • Slots per vault: Up to 256
  • Processing time: ~10-30 seconds per transfer
  • Chain support: Solana devnet only
  • Storage: Temporary centralized storage for demo purposes
  • Not audited: Do not use with real value

Storage Roadmap

Currently, encrypted data is stored in temporary centralized storage for demonstration purposes.

Future plans:

  • Decentralized storage (IPFS, Arweave)
  • On-chain encrypted blobs
  • User-controlled storage options

Full End-to-End Example

A complete example using the published SDK.

Two Things You Need
  1. 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
  2. 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/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:

  1. Generate keys locallycreateVault() registers with coprocessor, returns vaultKeyId
  2. Deploy vault program → Your Solana program (already done)
  3. Initialize on-chain → Links vault PDA to your vaultKeyId
  4. Deposit tokens → Coprocessor creates encrypted balance blob
  5. Encrypt & submit → Encrypt transfer, upload, call vault
  6. Decrypt result → Only you can decrypt with your clientKey

Run it:

npx ts-node your-e2e-test.ts

Support

GitHub Access

For access to the private repository, email team@zksdk.com