Skip to main content
ZK Compression provides Rust and Typescript clients to interact with compressed accounts and tokens on Solana.
TypeScript@lightprotocol/stateless.jsClient SDK for Compressed Accounts
TypeScript@lightprotocol/compressed-tokenClient SDK for Compressed Tokens
Rustlight-clientClient SDK for Compressed Accounts and Tokens

Key Points

  1. Derive a new address or fetch compressed account for on-chain verification.
  2. Fetch validity proof from the RPC that verifies a new address does not exist (create) and/or the account hash exists in the state tree (update, close, etc.).
  3. Pack accounts with the SDKs helper. Instructions require and . PackedAccounts converts their pubkeys to u8 indices pointing to accounts in the instruction.
  4. Build the instruction with the current account data, new data, packed accounts and validity proof.
  • Create
  • Update
  • Close
  • Reinitialize
  • Burn

Get Started

1

Setup

  • Typescript
  • Rust
Use the API documentation to look up specific function signatures, parameters, and return types.

1. Installation

  • npm
  • yarn
  • pnpm
npm install --save \
    @lightprotocol/stateless.js@0.22.1-alpha.1 \
    @lightprotocol/compressed-token@0.22.1-alpha.1 \
    @solana/web3.js

2. RPC Connection

Rpc is a thin wrapper extending Solana’s web3.js Connection class with compression-related endpoints.
  • Mainnet
  • Devnet
  • Localnet
const rpc = createRpc('https://mainnet.helius-rpc.com/?api-key=YOUR_API_KEY');
2

Address

Derive a persistent address as a unique identifier for your compressed account, similar to program-derived addresses (PDAs).You derive addresses in two scenarios:
  • At account creation - derive the address to create the account’s persistent identifier, then pass it to getValidityProofV0() in the address array
  • Before building instructions - derive the address to fetch existing accounts using rpc.getCompressedAccount()
  • Typescript
  • Rust
  • V1 Address Trees
  • V2 Address Trees
const addressTree = getDefaultAddressTreeInfo();
const seed = deriveAddressSeed(
  [Buffer.from('my-seed')],
  programId
);
const address = deriveAddress(
  seed,
  addressTree.tree
);
Like PDAs, compressed account addresses don’t have a private key; rather, they’re derived from the program that owns them.
  • The key difference to PDAs is compressed addresses are stored in an address tree and include this tree in the address derivation.
  • Different trees produce different addresses from identical seeds. You should check the address tree in your program.
The protocol maintains Merkle trees. You don’t need to initialize custom trees. Find the pubkeys for Merkle trees here.
3

Validity Proof

Transactions with compressed accounts must include a validity proof:
  • To create a compressed account, you prove the new address doesn’t already exist in the address tree.
  • In other instructions, you prove the compressed account hash exists in a state tree.
  • You can combine multiple addresses and hashes in one proof to optimize compute cost and instruction data.
You fetch a validity proof from your RPC provider that supports ZK Compression, such as Helius or Triton.
  • Typescript
  • Rust
  • Create
  • Update, Close, Reinit, Burn
const proof = await rpc.getValidityProofV0(
  [],
  [{
    address: bn(address.toBytes()),
    tree: addressTree.tree,
    queue: addressTree.queue
  }]
);
1. Pass these parameters:
  • Specify the new address, tree and queue pubkeys from the address tree TreeInfo.
  • When you create an account you don’t reference a compressed account hash in the hash array ([]). The account doesn’t exist in a state Merkle tree yet.
For account creation, you prove the address does not exist yet in the address tree.
2. The RPC returns:
  • The proof that the new address does not exist in the address tree. It is used in the instruction data.
  • rootIndices array with root index.
    • The root index points to the root in the address tree accounts root history array.
    • This root is used by the LightSystemProgram to verify the validity proof.

Optimize with Combined Proofs

Depending on the Merkle tree version (V1 or V2), you can prove in a single proof:
  • multiple addresses,
  • multiple account hashes, or
  • a combination of addresses and account hashes.
  • V1
  • V2
Account Hash-only1, 2, 3, 4, or 8 hashes
Address-only1 and 2 addresses
Mixed (hash + address)Any combination of
1, 2, 3, 4, or 8 account hashes and
1 or 2 new addresses
Advantages of combined proofs:
  • You only add one 128 byte validity proof to your instruction data.
  • This can optimize your transaction’s size to stay inside the 1232 byte instruction data limit.
  • Compute unit consumption is 100k CU per ValidityProof verification by the Light System Program.

Example Create Address & Update Account in one Proof

In this example, we generate one proof that proves that an account exists and that a new address does not exist yet.
  • Typescript
  • Rust
const proof = await rpc.getValidityProofV0(
  [{
    hash: compressedAccount.hash,
    tree: compressedAccount.treeInfo.tree,
    queue: compressedAccount.treeInfo.queue
  }],
  [{
    address: bn(address.toBytes()),
    tree: addressTree.tree,
    queue: addressTree.queue
  }]
);
1. Pass these parameters:
  • Specify one or more account hashes, tree and queue pubkeys from the compressed account’s TreeInfo.
  • Specify one or more new addresses with their tree and queue pubkeys from the address tree TreeInfo.
2. The RPC returns:
  • A single combined proof that proves both the account hash exists in the state tree and the new address does not exist in the address tree for your instruction data
  • rootIndices and leafIndices arrays with proof metadata to pack accounts.
4

Accounts

To interact with a compressed account you need System accounts such as the , and .Compressed account metadata (TreeInfo) includes Merkle tree pubkeys. To optimize instruction data we pack the pubkeys of TreeInfo into the u8 indices of PackedTreeInfo.The u8 indices point to the Merkle tree account in the instructions accounts. You can create the instructions accounts and indices with PackedAccounts.We recommend to append PackedAccounts after your program specific accounts and in anchor in remaining_accounts.
                                  PackedAccounts
                  ┌--------------------------------------------┐
[custom accounts] [pre accounts][system accounts][tree accounts]
                        ↑              ↑               ↑
                     Signers,      Light System    State trees,
                    fee payer        accounts     address trees,
Custom accounts are program-specific accounts you pass manually in your instruction, typically through Anchor’s account struct.
Optional, custom accounts (signers, PDAs for CPIs) and other accounts can be added to pre accounts. Pre accounts can simplify building the accounts for pinocchio and native programs.
Light System accounts are 6 required accounts for proof verification and CPI calls to update state and address trees.
1Verifies validity proofs, compressed account ownership checks, and CPIs the Account Compression Program to update tree accounts.
2CPI Signer
  • PDA to sign CPI calls from your program to the Light System Program.
  • Verified by the Light System Program during CPI.
  • Derived from your program ID.
3Registered Program PDAProvides access control to the Account Compression Program.
4Signs CPI calls from the Light System Program to the Account Compression Program.
5
  • Writes to state and address tree accounts.
  • Clients and the Account Compression Program do not interact directly — handled internally.
6Solana System Program used to transfer lamports.
Merkle tree accounts are the accounts of state tree and address trees that store compressed account hashes and addresses.
  • Typescript
  • Rust
  • Create
  • Update, Close, Reinit Burn
// 1. Initialize helper
const packedAccounts 
  = new PackedAccounts();

// 2. Add light system accounts
const systemAccountConfig
  = SystemAccountMetaConfig.new(programId);
packedAccounts.addSystemAccounts(systemAccountConfig);

// 3. Get indices for tree accounts
const addressMerkleTreePubkeyIndex 
  = packedAccounts.insertOrGet(addressTree);
const addressQueuePubkeyIndex 
  = packedAccounts.insertOrGet(addressQueue);

const packedAddressTreeInfo = {
  rootIndex: proofRpcResult.rootIndices[0],
  addressMerkleTreePubkeyIndex,
  addressQueuePubkeyIndex,
};

// 4. Get index for output state tree
const stateTreeInfos = await rpc.getStateTreeInfos();
const outputStateTree = selectStateTreeInfo(stateTreeInfos).tree;
const outputStateTreeIndex
  = packedAccounts.insertOrGet(outputStateTree);

// 5. Convert to Account Metas
const { remainingAccounts }
  = packedAccounts.toAccountMetas();
Depending on your instruction you must include different tree and queue accounts.
  • V1 Trees
  • V2 Trees
InstructionAddress TreeState TreeNullifier QueueOutput State Tree
Create--
Update / Close / Reinit-
Burn--
  • The Address tree is used to derive and store a new address (create-only)
  • The State tree is used to reference the existing compressed account hash. Therefore not used by create.
  • The Nullifier queue is used to nullify the existing compressed account hash to prevent double spending. Therefore not used by create.
  • The Output State tree is used to store the new or updated compressed account hash.
    • Create only - Choose any available state tree, or use a pre-selected tree to store the new compressed account.
    • Update/Close/Reinit - Use the state tree of the existing compressed account as output state tree.
    • Mixed instructions (create + update in same tx) - Use the state tree from the existing account as output state tree.
    • Burn - Burn does not produce output state and does not need an output state tree.
V2 is on Devnet and reduces compute unit consumption by up to 70%.
5

Instruction Data

Build your instruction data with the validity proof, tree account indices, and account data.
  • Typescript
  • Rust
  • Create
  • Update
  • Close
  • Reinit
  • Burn
const proof = {
  0: proofRpcResult.compressedProof,
};

const instructionData = {
  proof,
  addressTreeInfo: packedAddressTreeInfo,
  outputStateTreeIndex: outputStateTreeIndex,
  message,
};
  1. Include proof to prove the address does not exist in the address tree
  2. Specify Merkle trees to store address and account hash to where you packed accounts.
  3. Pass initial account data
  • When creating or updating multiple accounts in a single transaction, use one output state tree.
  • Minimize the number of different trees per transaction to keep instruction data light.
6

Instruction

Build the instruction with your program_id, accounts, and data.
  • Accounts combine your program-specific accounts and PackedAccounts.
  • Data includes your compressed accounts, validity proof and other instruction data.
  • Typescript
  • Rust
//             Accounts
// ┌-------------------------------┐
// .accounts()    .remainingAccounts()
// [custom]         [PackedAccounts]

const instruction = await program.methods
  .yourInstruction(instructionData)
  .accounts({
    signer: signer.publicKey,
  })
  .remainingAccounts(remainingAccounts)
  .instruction();
7

Send Transaction

Full Code Examples

  • Typescript
  • Rust
// create.ts
import * as anchor from "@coral-xyz/anchor";
import { Program, web3 } from "@coral-xyz/anchor";
import { Create } from "../target/types/create";
import idl from "../target/idl/create.json";
import {
  bn,
  CompressedAccountWithMerkleContext,
  confirmTx,
  createRpc,
  defaultStaticAccountsStruct,
  defaultTestStateTreeAccounts,
  deriveAddress,
  deriveAddressSeed,
  LightSystemProgram,
  PackedAccounts,
  Rpc,
  sleep,
  SystemAccountMetaConfig,
} from "@lightprotocol/stateless.js";
import * as assert from "assert";

const path = require("path");
const os = require("os");
require("dotenv").config();

const anchorWalletPath = path.join(os.homedir(), ".config/solana/id.json");
process.env.ANCHOR_WALLET = anchorWalletPath;

describe("test-anchor", () => {
  const program = anchor.workspace.Create as Program<Create>;
  const coder = new anchor.BorshCoder(idl as anchor.Idl);

  it("create compressed account", async () => {
    let signer = new web3.Keypair();
    let rpc = createRpc(
      "http://127.0.0.1:8899",
      "http://127.0.0.1:8784",
      "http://127.0.0.1:3001",
      {
        commitment: "confirmed",
      },
    );
    let lamports = web3.LAMPORTS_PER_SOL;
    await rpc.requestAirdrop(signer.publicKey, lamports);
    await sleep(2000);

    const outputStateTree = defaultTestStateTreeAccounts().merkleTree;
    const addressTree = defaultTestStateTreeAccounts().addressTree;
    const addressQueue = defaultTestStateTreeAccounts().addressQueue;

    const messageSeed = new TextEncoder().encode("message");
    const seed = deriveAddressSeed(
      [messageSeed, signer.publicKey.toBytes()],
      new web3.PublicKey(program.idl.address),
    );
    const address = deriveAddress(seed, addressTree);

    // Create compressed account with message
    const txId = await createCompressedAccount(
      rpc,
      addressTree,
      addressQueue,
      address,
      program,
      outputStateTree,
      signer,
      "Hello, compressed world!",
    );
    console.log("Transaction ID:", txId);

    // Wait for indexer to process the transaction
    const slot = await rpc.getSlot();
    await rpc.confirmTransactionIndexed(slot);

    let compressedAccount = await rpc.getCompressedAccount(bn(address.toBytes()));
    let myAccount = coder.types.decode(
      "MyCompressedAccount",
      compressedAccount.data.data,
    );

    console.log("Decoded data owner:", myAccount.owner.toBase58());
    console.log("Decoded data message:", myAccount.message);

    // Verify account data
    assert.ok(
      myAccount.owner.equals(signer.publicKey),
      "Owner should match signer public key"
    );
    assert.strictEqual(
      myAccount.message,
      "Hello, compressed world!",
      "Message should match the created message"
    );
  });
});

async function createCompressedAccount(
  rpc: Rpc,
  addressTree: anchor.web3.PublicKey,
  addressQueue: anchor.web3.PublicKey,
  address: anchor.web3.PublicKey,
  program: anchor.Program<Create>,
  outputStateTree: anchor.web3.PublicKey,
  signer: anchor.web3.Keypair,
  message: string,
) {
  const proofRpcResult = await rpc.getValidityProofV0(
    [],
    [
      {
        tree: addressTree,
        queue: addressQueue,
        address: bn(address.toBytes()),
      },
    ],
  );
  const systemAccountConfig = new SystemAccountMetaConfig(program.programId);
  let remainingAccounts = new PackedAccounts();
  remainingAccounts.addSystemAccounts(systemAccountConfig);

  const addressMerkleTreePubkeyIndex =
    remainingAccounts.insertOrGet(addressTree);
  const addressQueuePubkeyIndex = remainingAccounts.insertOrGet(addressQueue);
  const packedAddressTreeInfo = {
    rootIndex: proofRpcResult.rootIndices[0],
    addressMerkleTreePubkeyIndex,
    addressQueuePubkeyIndex,
  };
  const outputStateTreeIndex =
    remainingAccounts.insertOrGet(outputStateTree);
  let proof = {
    0: proofRpcResult.compressedProof,
  };
  const computeBudgetIx = web3.ComputeBudgetProgram.setComputeUnitLimit({
    units: 1000000,
  });
  let tx = await program.methods
    .createAccount(proof, packedAddressTreeInfo, outputStateTreeIndex, message)
    .accounts({
      signer: signer.publicKey,
    })
    .preInstructions([computeBudgetIx])
    .remainingAccounts(remainingAccounts.toAccountMetas().remainingAccounts)
    .signers([signer])
    .transaction();
  tx.recentBlockhash = (await rpc.getRecentBlockhash()).blockhash;
  tx.sign(signer);

  const sig = await rpc.sendTransaction(tx, [signer]);
  await confirmTx(rpc, sig);
  return sig;
}
Find all full code examples with Rust and Typescript tests here for the following instructions:
  • create - Initialize a new compressed account
  • update - Modify data of an existing compressed account
  • close - Close a compressed account (it can be initialized again).
  • reinit - Reinitialize a closed account
  • burn - Permanently delete a compressed account (it cannot be initialized again).
For help with debugging, see the Error Cheatsheet and AskDevin.

Next Steps

Get an overview to Compressed PDA guides and build a program.