Skip to main content

Overview

The transact_spl instruction enables private transactions for SPL tokens. It functions identically to the transact instruction but operates on SPL token accounts instead of native SOL. Each SPL token has its own dedicated merkle tree, initialized via initialize_tree_account_for_spl_token.

Function Signature

pub fn transact_spl(
    ctx: Context<TransactSpl>,
    proof: Proof,
    ext_data_minified: ExtDataMinified,
    encrypted_output1: Vec<u8>,
    encrypted_output2: Vec<u8>
) -> Result<()>

Parameters

proof
Proof
required
Zero-knowledge proof containing:
  • proof_a: First proof component (64 bytes)
  • proof_b: Second proof component (128 bytes)
  • proof_c: Third proof component (64 bytes)
  • root: Merkle tree root (32 bytes)
  • public_amount: Public transaction amount (32 bytes)
  • ext_data_hash: Hash of external data (32 bytes)
  • input_nullifiers: Array of 2 nullifiers (32 bytes each)
  • output_commitments: Array of 2 commitments (32 bytes each)
ext_data_minified
ExtDataMinified
required
Minified external data:
  • ext_amount: Token amount to deposit (positive) or withdraw (negative)
  • fee: Transaction fee in token base units
encrypted_output1
Vec<u8>
required
Encrypted data for the first output UTXO.
encrypted_output2
Vec<u8>
required
Encrypted data for the second output UTXO.

Accounts

tree_account
AccountLoader<MerkleTreeAccount>
required
Token-specific merkle tree. PDA: ["merkle_tree", mint.key()].
  • Mutable: Yes
nullifier0
Account<NullifierAccount>
required
First nullifier account. PDA: ["nullifier0", proof.input_nullifiers[0]].
  • Mutable: Yes
  • Initialized: Yes (created by this instruction)
nullifier1
Account<NullifierAccount>
required
Second nullifier account. PDA: ["nullifier1", proof.input_nullifiers[1]].
  • Mutable: Yes
  • Initialized: Yes (created by this instruction)
nullifier2
SystemAccount
required
Cross-check nullifier. PDA: ["nullifier0", proof.input_nullifiers[1]].
  • Must not exist
nullifier3
SystemAccount
required
Cross-check nullifier. PDA: ["nullifier1", proof.input_nullifiers[0]].
  • Must not exist
global_config
Account<GlobalConfig>
required
Global configuration. PDA: ["global_config"].
signer
Signer
required
Transaction signer.
  • Mutable: Yes
mint
Account<Mint>
required
SPL token mint account.
signer_token_account
Account<TokenAccount>
required
Signer’s token account (source for deposits).
  • Mutable: Yes
  • Must be owned by signer
  • Must have correct mint
recipient
UncheckedAccount
required
Recipient wallet account (owner of recipient_token_account).
recipient_token_account
Account<TokenAccount>
required
Recipient’s token account (destination for withdrawals).
  • Mutable: Yes
  • Token mint must match
  • Token authority must be recipient
tree_ata
Account<TokenAccount>
required
Tree’s associated token account.
  • Mutable: Yes
  • Auto-created if needed (init_if_needed)
  • Authority: global_config PDA
fee_recipient_ata
UncheckedAccount
required
Fee recipient’s associated token account.
  • Mutable: Yes
  • Must already exist for supported tokens
token_program
Program<Token>
required
SPL Token program.
associated_token_program
Program<AssociatedToken>
required
Associated Token program.
system_program
Program<System>
required
Solana system program.

Behavior

Deposits (ext_amount > 0)

  1. Validates token account ownership and mint
  2. Checks deposit amount is within limit
  3. Transfers tokens from signer_token_account to tree_ata
  4. Validates and transfers deposit fee to fee_recipient_ata
  5. Verifies zero-knowledge proof
  6. Appends commitments to token-specific merkle tree
  7. Emits SPL commitment events

Withdrawals (ext_amount < 0)

  1. Validates token accounts
  2. Verifies merkle root and proof
  3. Transfers tokens from tree_ata to recipient_token_account
  4. Transfers withdrawal fee to fee_recipient_ata
  5. Appends commitments to merkle tree
  6. Emits SPL commitment events

Code Example

import * as anchor from "@coral-xyz/anchor";
import { PublicKey } from "@solana/web3.js";
import { getAssociatedTokenAddress, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from "@solana/spl-token";

const depositAmount = 1000000; // 1 token with 6 decimals
const depositFee = 0;

// Get token accounts
const signerTokenAccount = await getAssociatedTokenAddress(
  mintPublicKey,
  userPublicKey
);

const recipientTokenAccount = await getAssociatedTokenAddress(
  mintPublicKey,
  recipientPublicKey
);

const treeAta = await getAssociatedTokenAddress(
  mintPublicKey,
  globalConfigPDA,
  true // allowOwnerOffCurve
);

const feeRecipientAta = await getAssociatedTokenAddress(
  mintPublicKey,
  feeRecipientPublicKey
);

// Get token-specific tree PDA
const [splTreePDA] = PublicKey.findProgramAddressSync(
  [Buffer.from("merkle_tree"), mintPublicKey.toBuffer()],
  program.programId
);

// Generate proof
const proof = await generateProof(inputs, outputs, extData);

const extDataMinified = {
  extAmount: new anchor.BN(depositAmount),
  fee: new anchor.BN(depositFee)
};

// Execute SPL deposit
const tx = await program.methods
  .transactSpl(
    proof,
    extDataMinified,
    encryptedOutput1,
    encryptedOutput2
  )
  .accounts({
    treeAccount: splTreePDA,
    nullifier0: nullifier0PDA,
    nullifier1: nullifier1PDA,
    nullifier2: nullifier2PDA,
    nullifier3: nullifier3PDA,
    globalConfig: globalConfigPDA,
    signer: userPublicKey,
    recipient: recipientPublicKey,
    mint: mintPublicKey,
    signerTokenAccount: signerTokenAccount,
    recipientTokenAccount: recipientTokenAccount,
    treeAta: treeAta,
    feeRecipientAta: feeRecipientAta,
    tokenProgram: TOKEN_PROGRAM_ID,
    associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
    systemProgram: anchor.web3.SystemProgram.programId
  })
  .signers([userKeypair])
  .rpc();

Allowed Tokens

  • USDC: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
  • USDT: Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB
  • ORE: oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp
  • ZEC: A7bdiYdS5GjqGFtxf17ppRHtDKPkkRqbKtR27dxvQXaS
  • stORE: sTorERYB6xAZ1SSbwpK3zoK2EEwbBrc7TZAzg1uCGiH
  • jlUSDC: 9BEcn9aPEmhSPbPQeFGjidRiEKki46fVQDyPpSQXPA2D
  • jlWSOL: 2uQsyo1fXXQkDtcpXnLofWy88PxcvnfH2L8FPSE62FVU

Events

This instruction emits two SplCommitmentData events:
pub struct SplCommitmentData {
    pub index: u64,
    pub mint_address: Pubkey,
    pub commitment: [u8; 32],
    pub encrypted_output: Vec<u8>,
}

Validations

signer_token_account.owner must equal signer.key()
signer_token_account.mint must equal mint.key()
Mint address must be in the allowed tokens list (unless localnet).
Same validations as the transact instruction apply.

Errors

InvalidTokenAccount
Error
Token account is not owned by the signer.
InvalidTokenAccountMintAddress
Error
Token account mint doesn’t match the provided mint.
InvalidMintAddress
Error
Mint address is not in the allowed tokens list.
UnknownRoot
Error
Merkle root not found in tree history.
ExtDataHashMismatch
Error
External data hash mismatch.
InvalidProof
Error
Zero-knowledge proof verification failed.
DepositLimitExceeded
Error
Deposit amount exceeds the token’s maximum deposit limit.

See Also

Build docs developers (and LLMs) love