PC-Token: Building the Program
Pre-Alpha Disclaimer: This is an early pre-alpha release for exploring the SDK and starting development only. There is no real encryption — all data is completely public and stored as plaintext on-chain. Do not submit any sensitive or real data. Encryption keys and the trust model are not final; do not rely on any encryption guarantees or key material until mainnet. All interfaces, APIs, and data formats are subject to change without notice. The Solana program and all on-chain data will be wiped periodically and everything will be deleted when we transition to Encrypt Alpha 1. This software is provided “as is” without warranty of any kind; use is entirely at your own risk and dWallet Labs assumes no liability for any damages arising from its use.
Account Layouts
Mint
Follows P-Token’s COption pattern for optional authorities:
#![allow(unused)]
fn main() {
pub struct Mint {
pub mint_authority_flag: [u8; 4], // COption
pub mint_authority: [u8; 32],
pub decimals: u8,
pub is_initialized: u8,
pub freeze_authority_flag: [u8; 4], // COption
pub freeze_authority: [u8; 32],
pub bump: u8,
}
}
TokenAccount
No plaintext fields. Balance is always encrypted:
#![allow(unused)]
fn main() {
pub struct TokenAccount {
pub mint: [u8; 32],
pub owner: [u8; 32],
pub balance: EUint64, // encrypted balance
pub delegate_flag: [u8; 4], // COption
pub delegate: [u8; 32],
pub state: u8, // Uninitialized/Initialized/Frozen
pub allowance: EUint64, // encrypted delegate allowance
pub close_authority_flag: [u8; 4], // COption
pub close_authority: [u8; 32],
pub bump: u8,
}
}
FHE Graphs
Transfer (conditional)
#![allow(unused)]
fn main() {
#[encrypt_fn]
fn transfer_graph(
from_balance: EUint64, to_balance: EUint64, amount: EUint64,
) -> (EUint64, EUint64) {
let sufficient = from_balance >= amount;
let new_from = if sufficient { from_balance - amount } else { from_balance };
let new_to = if sufficient { to_balance + amount } else { to_balance };
(new_from, new_to)
}
}
If the sender has insufficient funds, both balances remain unchanged — a privacy-preserving silent no-op. The chain cannot distinguish success from failure.
Delegated Transfer (composability)
#![allow(unused)]
fn main() {
#[encrypt_fn]
fn transfer_from_graph(
from_balance: EUint64, to_balance: EUint64,
allowance: EUint64, amount: EUint64,
) -> (EUint64, EUint64, EUint64) {
let sufficient_balance = from_balance >= amount;
let sufficient_allowance = allowance >= amount;
let can_transfer = sufficient_balance & sufficient_allowance;
// if either check fails → no-op
let new_from = if can_transfer { from_balance - amount } else { from_balance };
let new_to = if can_transfer { to_balance + amount } else { to_balance };
let new_allowance = if can_transfer { allowance - amount } else { allowance };
(new_from, new_to, new_allowance)
}
}
Both balance AND allowance are checked atomically in the encrypted domain.
Wrap / Unwrap
Wrap (SPL → pcToken)
- SPL transfer from user to vault (plaintext — the deposit is visible)
mint_to_graph(balance, amount)adds to encrypted balance- Amount ciphertext pre-created via gRPC (not
create_plaintext_typed)
Unwrap (pcToken → SPL)
Three-step flow that only reveals the withdrawal amount:
- UnwrapBurn —
unwrap_burn_graph(balance, amount) → (new_balance, burned).burned= amount if sufficient, 0 if not. Creates a temporaryWithdrawalReceipt. - UnwrapDecrypt — requests decryption of
burnedciphertext. - UnwrapComplete — verifies
burned == requested_amount. If yes → SPL transfer from vault. If no → no-op. Closes receipt.
The balance is never decrypted. Only the withdrawal amount appears on the temporary receipt.