Quick Access
Check out private transfer example:Step-By-Step Guide
1
Write your Solana program as you normally.
2
Add CPI hooks that create groups and permissions that enable restrictions
for specific delegated accounts.
3
Add CPI hooks to delegate, commit and undelegate state accounts through
Ephemeral Rollup sessions.
4
Deploy your Solana program using Anchor CLI.
5
Sign user message to retrieve authorization token from TEE endpoint.
6
Request for authorization token and send confidential transactions.
Private Transfer Example

| Software | Version | Installation Guide |
|---|---|---|
| Solana | 2.3.13 | Install Solana |
| Rust | 1.85.0 | Install Rust |
| Anchor | 0.32.1 | Install Anchor |
| Node | 24.10.0 | Install Node |
Code Snippets
- 1. Write program
- 2. Restrict
- 3. Delegate
- 4. Deploy
- 5. Authorize
- 6. Test
A simple transfer program where tokens are tracked through deposit accounts, and locked in a vault account:⬆️ Back to Top
Copy
Ask AI
#[program]
pub mod private_payments {
/// Initializes a deposit account for a user and token mint if it does not exist.
///
/// Sets up a new deposit account with zero balance for the user and token mint.
pub fn initialize_deposit(ctx: Context<InitializeDeposit>) -> Result<()> {
let deposit = &mut ctx.accounts.deposit;
deposit.set_inner(Deposit {
user: ctx.accounts.user.key(),
token_mint: ctx.accounts.token_mint.key(),
amount: 0,
});
Ok(())
}
/// Modifies the balance of a user's deposit account by transferring tokens in or out.
///
/// If `args.increase` is true, tokens are transferred from the user's token account to the deposit account.
/// If false, tokens are transferred from the deposit account back to the user's token account.
pub fn modify_balance(ctx: Context<ModifyDeposit>, args: ModifyDepositArgs) -> Result<()> {
let deposit = &mut ctx.accounts.deposit;
if args.increase {
transfer_checked(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
TransferChecked {
from: ctx.accounts.user_token_account.to_account_info(),
mint: ctx.accounts.token_mint.to_account_info(),
to: ctx.accounts.vault_token_account.to_account_info(),
authority: ctx.accounts.user.to_account_info(),
},
),
args.amount,
ctx.accounts.token_mint.decimals,
)?;
deposit.amount += args.amount;
} else {
let seeds = [
VAULT_PDA_SEED,
&ctx.accounts.token_mint.key().to_bytes(),
&[ctx.bumps.vault]
];
let signer_seeds = &[&seeds[..]];
transfer_checked(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
TransferChecked {
from: ctx.accounts.vault_token_account.to_account_info(),
mint: ctx.accounts.token_mint.to_account_info(),
to: ctx.accounts.user_token_account.to_account_info(),
authority: ctx.accounts.vault.to_account_info(),
},
signer_seeds,
),
args.amount,
ctx.accounts.token_mint.decimals,
)?;
deposit.amount -= args.amount;
}
Ok(())
}
/// ... Other instructions for delegation and privacy
}
/// Context for InitializeDeposit
#[derive(Accounts)]
pub struct InitializeDeposit<'info> {
#[account(mut)]
pub payer: Signer<'info>,
/// CHECK: Anyone can initialize the deposit
pub user: UncheckedAccount<'info>,
#[account(
init_if_needed,
payer = payer,
space = 8 + Deposit::INIT_SPACE,
seeds = [DEPOSIT_PDA_SEED, user.key().as_ref(), token_mint.key().as_ref()],
bump
)]
pub deposit: Account<'info, Deposit>,
pub token_mint: Account<'info, Mint>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
/// Context args for InitializeDeposit
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct ModifyDepositArgs {
pub amount: u64,
pub increase: bool,
}
/// Context for ModifyDeposit
#[derive(Accounts)]
pub struct ModifyDeposit<'info> {
#[account(mut)]
pub payer: Signer<'info>,
pub user: Signer<'info>,
#[account(
init_if_needed,
payer = payer,
space = 8 + Vault::INIT_SPACE,
seeds = [VAULT_PDA_SEED, deposit.token_mint.as_ref()],
bump,
)]
pub vault: Account<'info, Vault>,
#[account(
mut,
seeds = [DEPOSIT_PDA_SEED, deposit.user.as_ref(), deposit.token_mint.as_ref()],
bump,
has_one = user,
has_one = token_mint,
)]
pub deposit: Account<'info, Deposit>,
#[account(
init_if_needed,
payer = payer,
associated_token::mint = token_mint,
associated_token::authority = user,
)]
pub user_token_account: Account<'info, TokenAccount>,
#[account(
init_if_needed,
payer = payer,
associated_token::mint = token_mint,
associated_token::authority = vault,
)]
pub vault_token_account: Account<'info, TokenAccount>,
pub token_mint: Account<'info, Mint>,
pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}
/// A deposit account for a user and token mint.
#[account]
#[derive(InitSpace)]
pub struct Deposit {
pub user: Pubkey,
pub token_mint: Pubkey,
pub amount: u64,
}
/// A vault storing deposited tokens.
/// Has a dummy field because Anchor requires it.
#[account]
#[derive(InitSpace)]
pub struct Vault {
_dummy: u8,
}
/// ... Other context and accounts for delegation and privacy
Define account-level permissions in two steps:⬆️ Back to Top
- Create user group
- Create permissions for state accounts and user group:
Copy
Ask AI
#[program]
pub mod private_payments {
/// Creates a permission group and permission for a deposit account using the external permission program.
/// Calls out to the permission program to create a group and permission for the deposit account.
pub fn create_permission(ctx: Context<CreatePermission>, id: Pubkey) -> Result<()> {
let CreatePermission {
payer,
permission,
permission_program,
group,
deposit,
user,
system_program,
} = ctx.accounts;
CreateGroupCpiBuilder::new(&permission_program)
.group(&group)
.id(id)
.members(vec![user.key()])
.payer(&payer)
.system_program(system_program)
.invoke()?;
CreatePermissionCpiBuilder::new(&permission_program)
.permission(&permission)
.delegated_account(&deposit.to_account_info())
.group(&group)
.payer(&payer)
.system_program(system_program)
.invoke_signed(&[&[
DEPOSIT_PDA_SEED,
user.key().as_ref(),
deposit.token_mint.as_ref(),
&[ctx.bumps.deposit],
]])?;
Ok(())
}
/// ... other instructions
}
#[derive(Accounts)]
pub struct CreatePermission<'info> {
#[account(mut)]
pub payer: Signer<'info>,
/// CHECK: Anyone can create the permission
pub user: UncheckedAccount<'info>,
#[account(
seeds = [DEPOSIT_PDA_SEED, user.key().as_ref(), deposit.token_mint.as_ref()],
bump
)]
pub deposit: Account<'info, Deposit>,
/// CHECK: Checked by the permission program
#[account(mut)]
pub permission: UncheckedAccount<'info>,
/// CHECK: Checked by the permission program
#[account(mut)]
pub group: UncheckedAccount<'info>,
/// CHECK: Checked by the permission program
pub permission_program: UncheckedAccount<'info>,
pub system_program: Program<'info, System>,
}
/// Other context and accounts ...
Enforce privacy of accounts through delegation to validator in TEE:⬆️ Back to Top
Copy
Ask AI
#[ephemeral] // This adds undelegation instruction for ER validator
#[program]
pub mod private_payments {
/// Delegates the deposit account to the ephemeral rollups delegate program.
/// Uses the ephemeral rollups delegate CPI to delegate the deposit account.
pub fn delegate(ctx: Context<DelegateDeposit>, user: Pubkey, token_mint: Pubkey) -> Result<()> {
let validator = ctx.accounts.validator.as_ref().map(|v| v.key());
ctx.accounts.delegate_deposit(
&ctx.accounts.payer,
&[DEPOSIT_PDA_SEED, user.as_ref(), token_mint.as_ref()],
DelegateConfig {
validator,
..DelegateConfig::default()
},
)?;
Ok(())
}
/// Commits and undelegates the deposit account from the ephemeral rollups program.
/// Uses the ephemeral rollups SDK to commit and undelegate the deposit account.
/// Use session keys for better UX
#[session_auth_or(
ctx.accounts.user.key() == ctx.accounts.source_deposit.user,
ErrorCode::Unauthorized
)]
pub fn undelegate(ctx: Context<UndelegateDeposit>) -> Result<()> {
commit_and_undelegate_accounts(
&ctx.accounts.payer,
vec![&ctx.accounts.deposit.to_account_info()],
&ctx.accounts.magic_context,
&ctx.accounts.magic_program,
)?;
Ok(())
}
/// Transfers a specified amount from one user's deposit account to another's for the same token mint.
/// Only updates the internal accounting; does not move actual tokens.
/// Use session keys for better UX
#[session_auth_or(
ctx.accounts.user.key() == ctx.accounts.source_deposit.user,
ErrorCode::Unauthorized
)]
pub fn transfer_deposit(ctx: Context<TransferDeposit>, amount: u64) -> Result<()> {
let source_deposit = &mut ctx.accounts.source_deposit;
let destination_deposit = &mut ctx.accounts.destination_deposit;
source_deposit.amount -= amount;
destination_deposit.amount += amount;
Ok(())
}
/// ... other instructions
}
/// Other context and accounts ...
/// Context for DelegateDeposit
#[delegate]
#[derive(Accounts)]
#[instruction(user: Pubkey, token_mint: Pubkey)]
pub struct DelegateDeposit<'info> {
#[account(mut)]
pub payer: Signer<'info>,
/// CHECK: Checked by the delegate program
pub validator: Option<AccountInfo<'info>>,
/// CHECK: Checked counter accountby the delegate program
#[account(
mut,
del,
seeds = [DEPOSIT_PDA_SEED, user.as_ref(), token_mint.as_ref()],
bump,
)]
pub deposit: AccountInfo<'info>,
}
/// Context for UndelegateDeposit
#[commit]
#[derive(Accounts, Session)]
pub struct UndelegateDeposit<'info> {
/// CHECK: Matched against the deposit account
pub user: AccountInfo<'info>,
#[account(mut)]
pub payer: Signer<'info>,
#[session(
signer = payer,
authority = user.key()
)]
pub session_token: Option<Account<'info, SessionToken>>,
#[account(
mut,
seeds = [DEPOSIT_PDA_SEED, user.key().as_ref(), deposit.token_mint.as_ref()],
bump
)]
pub deposit: Account<'info, Deposit>,
}
/// Context for TransferDeposit for ER
#[derive(Accounts, Session)]
pub struct TransferDeposit<'info> {
/// CHECK: Matched against the deposit account
pub user: AccountInfo<'info>,
#[account(mut)]
pub payer: Signer<'info>,
#[session(
signer = payer,
authority = user.key()
)]
pub session_token: Option<Account<'info, SessionToken>>,
#[account(
mut,
seeds = [
DEPOSIT_PDA_SEED,
source_deposit.user.as_ref(),
source_deposit.token_mint.as_ref()
],
bump,
has_one = user,
has_one = token_mint,
constraint = source_deposit.user != destination_deposit.user,
)]
pub source_deposit: Account<'info, Deposit>,
#[account(
mut,
seeds = [
DEPOSIT_PDA_SEED,
destination_deposit.user.as_ref(),
destination_deposit.token_mint.as_ref()
],
bump,
has_one = token_mint,
)]
pub destination_deposit: Account<'info, Deposit>,
pub token_mint: Account<'info, Mint>,
pub system_program: Program<'info, System>,
}
Delegationis the process of transferring ownership of one or more of your program’sPDAsto the delegation program. Ephemeral Validators will then be able to use thePDAsto perform transactions in the SVM runtime.
Commitis the process of updating the state of thePDAsfrom ER to the base layer. After the finalization process, thePDAsremain locked on base layer.
Specify your preferred delegation config such as auto commits and specific ER validator:Undelegationis the process of transferring ownership of thePDAsback to your program. On undelegation, the state is committed and it trigger the finalization process. Once state it validated, thePDAsare unlocked and can be used as normal on base layer.
These public validators are supported for development. Make sure to add the specific ER validator account when delegating:
- Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57 - EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e - US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd - TEE (tee.magicblock.app):
FnE6VJT5QNZdedZPnCoLsARgBwoE6DeJNjBs2H1gySXA - Local ER (localhost):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
Now you’re program is upgraded and ready! Build and deploy to the desired cluster:⬆️ Back to Top
Copy
Ask AI
anchor build && anchor deploy
Set up interaction with ER RPC in TEE:⬆️ Back to Top
- Verify integrity of TEE RPC via
https://pccs.phala.network/tdx/certification/v4 - Request an authorization token for user to interact with TEE endpoint
web3js
Copy
Ask AI
import {
verifyTeeRpcIntegrity,
getAuthToken,
} from "@magicblock-labs/ephemeral-rollups-sdk";
// Verify the integrity of TEE RPC
const isVerified = await verifyTeeRpcIntegrity(EPHEMERAL_RPC_URL);
// Get AuthToken before making request to TEE
const token = await getAuthToken(
EPHEMERAL_RPC_URL,
wallet.publicKey,
(message: Uint8Array) =>
Promise.resolve(nacl.sign.detached(message, wallet.secretKey))
);
Test private payments program using the authToken:
https://tee.magicblock.app?token=${token}- Initialize user deposit account (also for recipient)
- Deposit tokens
- Set permission for deposit
- Delegate deposit
- Transfer privately between delegated deposits
- Undelegate deposit to Solana
- Withdraw deposit
web3js
Copy
Ask AI
import {
DELEGATION_PROGRAM_ID,
PERMISSION_PROGRAM_ID,
permissionPdaFromAccount,
groupPdaFromId,
waitUntilPermissionActive,
GetCommitmentSignature,
} from "@magicblock-labs/ephemeral-rollups-sdk";
// [1] Initialize deposit account for user (also required for receiver)
const deposit = PublicKey.findProgramAddressSync(
[Buffer.from(DEPOSIT_PDA_SEED), user.toBuffer(), tokenMint.toBuffer()],
program.programId
)[0];
const initHash = await program.methods
.initializeDeposit()
.accountsPartial({
payer: program.provider.publicKey,
user,
deposit,
tokenMint,
tokenProgram: TOKEN_PROGRAM_ID,
})
.rpc();
// [2] Add tokens to deposit by using a central vault (WIP with Token Program)
// User deposits tokens into vault and receives deposit PDA receipt / updates
const vault = PublicKey.findProgramAddressSync(
[Buffer.from(VAULT_PDA_SEED), tokenMint.toBuffer()],
program.programId
)[0];
const depositHash = await program.methods
.modifyBalance({ amount: tokenAmount, increase: true })
.accountsPartial({
payer: program.provider.publicKey,
user: wallet.publicKey,
vault: vault,
deposit: deposit,
userTokenAccount: getAssociatedTokenAddressSync(
tokenMint,
wallet.publicKey,
true,
TOKEN_PROGRAM_ID
),
vaultTokenAccount: getAssociatedTokenAddressSync(
tokenMint,
vault,
true,
TOKEN_PROGRAM_ID
),
tokenMint,
tokenProgram: TOKEN_PROGRAM_ID,
})
.rpc();
// [3] Set permission for deposit account with required PDAs, and wait until permission is active
const id = Keypair.generate().publicKey;
const permission = permissionPdaFromAccount(deposit);
const group = groupPdaFromId(id);
const createPermissionHash = await program.methods
.createPermission(id)
.accountsPartial({
payer: program.provider.publicKey,
user,
deposit,
permission,
group,
permissionProgram: PERMISSION_PROGRAM_ID,
})
.rpc();
await waitUntilPermissionActive(ephemeralRpcEndpoint, deposit);
// [4] Delegate deposit account to TEE validator, and enforce privacy
const teeValidator = new PublicKey(
FnE6VJT5QNZdedZPnCoLsARgBwoE6DeJNjBs2H1gySXA
);
const delegateHash = await program.methods
.delegate(user, tokenMint)
.accountsPartial({
payer: program.provider.publicKey,
teeValidator,
deposit,
})
.rpc();
// [5] Transfer deposit amounts privately on ER on TEE
// User transfers tokens by updating deposit accounts (initialized destination deposit is required)
const sourceDeposit = PublicKey.findProgramAddressSync(
[Buffer.from(DEPOSIT_PDA_SEED), user.toBuffer(), tokenMint.toBuffer()],
program.programId
)[0];
const destinationDeposit = PublicKey.findProgramAddressSync(
[Buffer.from(DEPOSIT_PDA_SEED), receiver.toBuffer(), tokenMint.toBuffer()],
program.programId
)[0];
const ephemTransferHash = await programEphemeralRpcEndpointWithAuthToken.methods
.transferDeposit(new BN(amount * Math.pow(10, 6)))
.accountsPartial({
sessionToken: null,
payer: program.provider.publicKey,
user: program.provider.publicKey,
sourceDeposit,
destinationDeposit,
tokenMint,
})
.rpc();
// [6] Undelegate deposit account, and wait for commitment on Solana
const ephemUndelegateHash =
await programEphemeralRpcEndpointWithAuthToken.methods
.undelegate()
.accountsPartial({
sessionToken,
payer: sessionKp.publicKey,
user: wallet.publicKey,
deposit: senderDepositPda,
})
.rpc();
const txBase = await GetCommitmentSignature(
ephemUndelegateHash,
ephemeralRpcEndpointWithAuthToken
);
// [7] Withdraw tokens from central vault with deposit account
// User claims tokens from vault by updating deposit PDA
const vault = PublicKey.findProgramAddressSync(
[Buffer.from(VAULT_PDA_SEED), tokenMint.toBuffer()],
program.programId
)[0];
const withdrawHash = await program.methods
.modifyBalance({ amount: tokenAmount, increase: false })
.accountsPartial({
payer: program.provider.publicKey,
user: wallet.publicKey,
vault: vault,
deposit: deposit,
userTokenAccount: getAssociatedTokenAddressSync(
tokenMint,
wallet.publicKey,
true,
TOKEN_PROGRAM_ID
),
vaultTokenAccount: getAssociatedTokenAddressSync(
tokenMint,
vault,
true,
TOKEN_PROGRAM_ID
),
tokenMint,
tokenProgram: TOKEN_PROGRAM_ID,
})
.rpc();
These public validators are supported for development. Make sure to add the specific ER validator account when delegating:
- Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57 - EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e - US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd - TEE (tee.magicblock.app):
FnE6VJT5QNZdedZPnCoLsARgBwoE6DeJNjBs2H1gySXA - Local ER (localhost):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
Quick Access
Check out private transfer example:⬆️ Back to TopSolana Explorer
Get insights about your transactions and accounts on Solana:Solana RPC Providers
Send transactions and requests through existing RPC providers:Solana Validator Dashboard
Find real-time updates on Solana’s validator infrastructure:Server Status
Subscribe to Solana’s and MagicBlock’s server status:Solana Status
Subscribe to Solana Server Updates
MagicBlock Status
Subscribe to MagicBlock Server Status
MagicBlock Products
Ephemeral Rollup (ER)
Execute real-time, zero-fee transactions securely on Solana.
Private Ephemeral Rollup (PER)
Protect sensitive data with privacy-preserving computation.
Verifiable Randomness Function (VRF)
Generate provably fair randomness directly on-chain.

