# Agent skills
Source: https://www.zkcompression.com/ai-tools/agent-skills
Install structured agent skill files for Token APIs and PDA accounts. Skills tell AI coding agents what they can do with ZK Compression.
## Agent Skill for Orchestration
View or install [`/skill.md`](https://zkcompression.com/skill.md). If you're building with agents, start here.
```bash theme={null}
npx skills add https://zkcompression.com
```
## Dedicated Agent Skills
Install dedicated agent skills with this command:
```
npx skills add Lightprotocol/skills
```
Add the marketplace and install:
```
/plugin marketplace add Lightprotocol/skills
/plugin install solana-rent-free-dev
```
1. Open Settings (**Cmd+Shift+J** / **Ctrl+Shift+J**)
2. Navigate to **Rules & Commands** → **Project Rules** → **Add Rule** → **Remote Rule (GitHub)**
3. Enter: `https://github.com/Lightprotocol/skills.git`
| Use case | Skill |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| For token use cases on Solana, such as token distribution, stablecoin payments and more. Works with Token-2022, Privy, Wallet Adapter similarly to SPL. | [compressed-token](https://github.com/Lightprotocol/skills/tree/main/skills/compressed-token) |
| For program development on Solana with infrequently accessed state, such as per-user state, DePIN registrations, nullifiers, or custom compressed accounts | [compressed-pda](https://github.com/Lightprotocol/skills/tree/main/skills/compressed-pda) |
| For custom ZK Solana programs and privacy-preserving applications to prevent double spending | [zk](https://github.com/Lightprotocol/skills/tree/main/skills/zk) |
| For testing with Light Protocol programs and clients on localnet, devnet, and mainnet validation | [testing](https://github.com/Lightprotocol/skills/tree/main/skills/testing) |
| For questions about compressed accounts, Light SDK, Solana development, Claude Code features, or agent skills | [ask-mcp](https://github.com/Lightprotocol/skills/tree/main/skills/ask-mcp) |
> View all skills here: [https://github.com/Lightprotocol/skills](https://github.com/Lightprotocol/skills).
All skills are included and are auto-discovered based on context. Ask about compressed tokens, compressed PDAs, ZK programs, or testing and the agent uses the relevant skill automatically.
# MCP
Source: https://www.zkcompression.com/ai-tools/mcp
Connect AI apps to Light Protocol data sources and tools via the Model Context Protocol (MCP). Includes installation for Claude Code, Codex, Cursor, and more.
The Model Context Protocol (MCP) is an open standard to connect AI apps to data sources and tools. The DeepWiki MCP server provides access to the Light Protocol repository with its search capabilities (AskDevin).
## Installation
```bash theme={null}
claude mcp add -s user -t http deepwiki https://mcp.deepwiki.com/mcp
claude mcp add -s user -t http zkcompression https://www.zkcompression.com/mcp
```
**We recommend to create a command** that includes the tools listed below.
Simply **copy paste this snippet**.
```markdown expandable theme={null}
---
argument-hint:
description: Query Light Protocol repository and Solana resources via DeepWiki MCP for precise technical answers and help with debugging
allowed-tools: mcp__deepwiki__*
---
# /ask-deepwiki
Answer: $ARGUMENTS
Use DeepWiki MCP to provide precise technical answers.
## Step 1: Identify Repository Scope
1. **State understanding and plan**
2. **If question is vague, ask for clarification:**
- What specific component or feature?
- What problem are you solving?
- What level of detail needed (overview vs implementation)?
3. **Repository mapping:**
- ZK Compression/Light Protocol: `Lightprotocol/light-protocol`
- Solana fundamentals: `solana-labs/solana`
- Anchor framework: `solana-foundation/anchor`
- Complex questions: Query multiple repositories
4. **Refine question to use:**
- Exact component names: `CompressedAccountMeta` not "account metadata"
- Specific operations: "verifies proof" not "handles proof"
- Concrete function names or error messages when available
## Step 2: Query DeepWiki
For the identified repository, call in sequence:
1. `mcp__deepwiki__read_wiki_structure("repo-owner/repo-name")`
2. `mcp__deepwiki__read_wiki_contents("repo-owner/repo-name")`
3. `mcp__deepwiki__ask_question("repo-owner/repo-name", refined_question)`
Query multiple repositories if question spans different systems.
## Step 3: Format Response
**Structure:**
1. Direct answer with technical explanation
2. Specific implementations and data structures
3. Code examples with inline comments
4. Source references (`file:line` from DeepWiki)
5. Related concepts if relevant
**Language precision:**
NEVER use vague verbs:
- "handles", "manages", "processes", "enables", "provides"
ALWAYS use exact names:
- Functions: `LightAccount::new_init()`, `derive_address()`
- Types: `CompressedAccountMeta`, `ValidityProof`, `PackedAddressTreeInfo`
- Operations: "nullifies hash", "appends to state tree", "verifies proof"
- Fields: `tree_info`, `address`, `output_state_tree_index`
Include `file:line` references from DeepWiki responses.
## Notes
- Always include source file references from DeepWiki responses
- Provide runnable code examples for implementation questions
- Ask follow-up questions to DeepWiki for clarification when needed
```
```bash theme={null}
codex mcp add zkcompression -- npx -y mcp-remote@latest https://www.zkcompression.com/mcp
codex mcp add deepwiki -- npx -y mcp-remote@latest https://mcp.deepwiki.com/mcp
```
```json theme={null}
{
"mcpServers": {
"deepwiki": {
"serverUrl": "https://mcp.deepwiki.com/sse"
},
"zkcompression": {
"command": "npx",
"args": ["-y", "mcp-remote@latest", "https://www.zkcompression.com/mcp"]
}
}
}
```
## DeepWiki MCP-Tools
* **read\_wiki\_structure** - Get a list of documentation topics for a GitHub repository
```bash theme={null}
mcp__deepwiki__read_wiki_structure("Lightprotocol/light-protocol")
```
* **read\_wiki\_contents** - View documentation about a GitHub repository
```bash theme={null}
mcp__deepwiki__read_wiki_contents("Lightprotocol/light-protocol")
```
* **ask\_question** - Ask any question about the GitHub repository and get a context-grounded response
```bash theme={null}
mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "your question")
```
You can specify any public GitHub repo that is indexed with DeepWiki. When you connect to the MCP, you specify the repository when calling the tools. Learn more [here](https://docs.devin.ai/work-with-devin/deepwiki-mcp).
If you don't have the MCP installed we recommend to use [AskDevin](https://deepwiki.com/Lightprotocol/light-protocol) in your browser.
# AI tools
Source: https://www.zkcompression.com/ai-tools/overview
AI tools for working with Light Protocol and ZK Compression. Includes docs search, agent skills, MCP server, and ready-to-use prompts.
## For docs
* **Docs AI Search** - Search documentation with AI in the search bar.
* **Markdown Export** - Append `.md` to any page URL for raw markdown.
- **View [`/llms.txt`](https://zkcompression.com/llms.txt)** for an index of the docs - It lists key pages with descriptions
so agents can navigate to answers quickly.
## For development
Install structured skill files that tell agents what they can do with Light Protocol.
Connect AI apps to Light Protocol data sources via the Model Context Protocol.
Ready-to-use prompts for your AI coding agent.
# AI Prompts
Source: https://www.zkcompression.com/ai-tools/prompts
Ready-to-use AI prompts for Light Protocol. Copy a prompt, paste into your AI coding agent, and let it implement the recipe in your project.
## Token Distribution
Copy the prompt below or view the [guide](/compressed-tokens/token-distribution).
```text theme={null}
---
description: Distribute compressed tokens via airdrop
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Distribute compressed tokens via airdrop
Context:
- Guide: https://zkcompression.com/compressed-tokens/token-distribution
- Skills and resources index: https://zkcompression.com/skill.md
- Dedicated skill: https://github.com/Lightprotocol/skills/tree/main/skills/airdrop
- Packages: @lightprotocol/compressed-token, @lightprotocol/stateless.js, @solana/spl-token
- Example repo: https://github.com/Lightprotocol/example-token-distribution
- Webapp alternative: https://airship.helius.dev/ (Airship by Helius Labs, up to 200k recipients)
Key APIs: LightTokenProgram.compress(), getTokenPoolInfos(), selectTokenPoolInfo(), getStateTreeInfos(), selectStateTreeInfo(), buildAndSignTx(), sendAndConfirmTx()
### 1. Index project
- Grep `LightTokenProgram|compress|getTokenPoolInfos|selectTokenPoolInfo|getStateTreeInfos|@lightprotocol|airdrop|distribution` across src/
- Glob `**/*.ts` for project structure
- Identify: existing airdrop/distribution logic, token minting setup, recipient list format
- Check package.json for existing @lightprotocol/* or @solana/spl-token dependencies
- Task subagent (Grep/Read/WebFetch) if project has multiple packages to scan in parallel
### 2. Read references
- WebFetch the guide above — review all three tabs (Localnet Guide, Simple Airdrop, Batched)
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: what scale? (<10k recipients = simple airdrop, 10k+ = batched)
- AskUserQuestion: localnet testing first, or production deploy?
- AskUserQuestion: do you have an existing SPL mint, or need to create one?
- AskUserQuestion: do you need decompression/claim functionality, or just direct distribution?
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan
- For simple airdrop: create mint → mint SPL tokens → LightTokenProgram.compress() with recipients array
- For batched: create instruction batches → manage blockhash refresh → sign and send with retry logic
- Address lookup table needed for production (mainnet: 9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ)
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- Add deps if missing: Bash `npm install @lightprotocol/compressed-token @lightprotocol/stateless.js @solana/spl-token`
- Set up RPC: `createRpc(RPC_ENDPOINT)` with a ZK Compression endpoint (Helius, Triton)
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- Bash `tsc --noEmit`
- Bash run existing test suite or execute localnet test airdrop
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work
```
## Compressed PDA
Copy the prompt below or view the [guide](/compressed-pdas/nullifier-pda).
```text theme={null}
---
description: Create rent-free nullifier PDAs to prevent duplicate actions
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Create rent-free nullifier PDAs to prevent duplicate actions
Context:
- Guide: https://zkcompression.com/compressed-pdas/nullifier-pda
- Skills and resources index: https://zkcompression.com/skill.md
- Dedicated skill: https://github.com/Lightprotocol/skills/tree/main/skills/zk-nullifier
- Rust crates: light-nullifier-program, light-client
- TS packages: @lightprotocol/nullifier-program, @lightprotocol/stateless.js
- Example: https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/actions/create_nullifier.rs
- Program source: https://github.com/Lightprotocol/nullifier-program/
Key APIs:
- Rust: create_nullifier_ix(), fetch_proof(), build_instruction(), derive_nullifier_address()
- TS: createNullifierIx(), fetchProof(), buildInstruction(), deriveNullifierAddress()
### 1. Index project
- Grep `nullifier|create_nullifier|createNullifierIx|deriveNullifierAddress|NFLx5WGPrTHHvdRNsidcrNcLxRruMC92E4yv7zhZBoT` across src/
- Glob `**/*.rs` and `**/*.ts` for project structure
- Identify: existing transaction building, duplicate prevention logic, payment flow
- Check Cargo.toml or package.json for existing light-* dependencies
- Task subagent (Grep/Read/WebFetch) if project has multiple packages to scan in parallel
### 2. Read references
- WebFetch the guide above — review both Rust and TS code samples
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: Rust or TypeScript?
- AskUserQuestion: what is the goal? (prevent duplicate payments, idempotent instruction execution, other use case)
- AskUserQuestion: do you need the helper (create_nullifier_ix) or manual proof fetching (fetch_proof + build_instruction)?
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan: which files to modify, what code to add, dependency changes
- Follow the guide's pattern: create unique 32-byte ID → build nullifier instruction → prepend to transaction
- If checking existence is needed, add derive_nullifier_address + get_compressed_account check
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- For Rust: Bash `cargo add light-nullifier-program@0.1 light-client@0.23`
- For TypeScript: Bash `npm install @lightprotocol/nullifier-program @lightprotocol/stateless.js@^0.23.0`
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- Rust: Bash `cargo check` + `cargo test` if tests exist
- TypeScript: Bash `tsc --noEmit` + run existing test suite if present
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work
```
Copy the prompt below or view the [guide](/compressed-pdas/guides/client-guide).
```text theme={null}
---
description: Write client code to interact with compressed PDA programs
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Write client code to interact with compressed PDA programs
Context:
- Guide: https://zkcompression.com/compressed-pdas/guides/client-guide
- Skills and resources index: https://zkcompression.com/skill.md
- TS packages: @lightprotocol/stateless.js, @lightprotocol/compressed-token, @solana/web3.js
- Rust crates: light-client, light-sdk
- TS example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/create/tests
- Rust example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/create/programs/create/tests
- All operations: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor
Key APIs:
- TS: createRpc(), deriveAddressSeedV2(), deriveAddressV2(), getValidityProofV0(), PackedAccounts, SystemAccountMetaConfig, getStateTreeInfos(), selectStateTreeInfo()
- Rust: LightClientConfig, LightClient, derive_address(), get_validity_proof(), PackedAccounts, SystemAccountMetaConfig, get_random_state_tree_info()
### 1. Index project
- Grep `@lightprotocol|stateless\.js|createRpc|deriveAddress|getValidityProof|PackedAccounts|light_client|light_sdk|derive_address|get_validity_proof` across src/
- Glob `**/*.ts` and `**/*.rs` for project structure
- Identify: RPC setup, existing compressed account operations, program ID references
- Check package.json or Cargo.toml for existing light-* dependencies
- Task subagent (Grep/Read/WebFetch) if project has multiple packages to scan in parallel
### 2. Read references
- WebFetch the guide above — review both TypeScript and Rust code samples
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: TypeScript or Rust?
- AskUserQuestion: what is the goal? (new client from scratch, add client calls to existing project, migrate from regular accounts)
- AskUserQuestion: which operations? (create only, full CRUD, specific subset like create + update)
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan: which files to modify, what code to add, dependency changes
- Follow the guide's step order: Setup → Address derivation → Validity proof → PackedAccounts → Instruction data → Build instruction → Send transaction
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- For TypeScript: Bash `npm install @lightprotocol/stateless.js @lightprotocol/compressed-token @solana/web3.js`
- For Rust: Bash `cargo add light-client@0.23 light-sdk@0.23`
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- TypeScript: Bash `tsc --noEmit` + run existing test suite if present
- Rust: Bash `cargo check` + `cargo test` if tests exist
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work
```
Copy the prompt below or view the [guide](/compressed-pdas/guides/how-to-create-compressed-accounts).
```text theme={null}
---
description: Build a program that creates compressed accounts with addresses
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Build a program that creates compressed accounts with addresses
Context:
- Guide: https://zkcompression.com/compressed-pdas/guides/how-to-create-compressed-accounts
- Skills and resources index: https://zkcompression.com/skill.md
- Crate: light-sdk (LightAccount, CpiAccounts, LightSystemProgramCpi, derive_light_cpi_signer!)
- Anchor example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/create
- Native Rust example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/create
Key SDK API: LightAccount::new_init(), derive_address()
### 1. Index project
- Grep `declare_id|#\[program\]|entrypoint!|Pubkey|AccountInfo|seeds|init|payer|space` across src/
- Glob `**/*.rs` and `**/Cargo.toml` for project structure
- Identify: program ID, existing instructions, account structs, framework (Anchor or native)
- Read Cargo.toml — note existing dependencies and Solana SDK version
- Task subagent (Grep/Read/WebFetch) if project has multiple crates to scan in parallel
### 2. Read references
- WebFetch the guide above — review both Anchor and Native Rust code samples
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: what is the goal? (new program from scratch, add account creation to existing program, migrate from regular accounts)
- AskUserQuestion: Anchor or Native Rust framework?
- AskUserQuestion: does the program already have compressed account instructions, or is this the first one?
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan: which files to modify, what code to add, dependency changes
- Follow the guide's step order: Dependencies → Constants → Account Struct → Instruction Data → Derive Address → Address Tree Check → Initialize Account → Light System Program CPI
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- Add deps: Bash `cargo add light-sdk@0.23` (add `anchor_lang@0.31` for Anchor or `solana-program@2.2` + `borsh@0.10` for Native Rust)
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- Bash `cargo build-sbf` or `anchor build`
- Bash `cargo test-sbf` or `anchor test` if tests exist
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work
```
Copy the prompt below or view the [guide](/compressed-pdas/guides/how-to-update-compressed-accounts).
```text theme={null}
---
description: Build a program that updates compressed accounts
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Build a program that updates compressed accounts
Context:
- Guide: https://zkcompression.com/compressed-pdas/guides/how-to-update-compressed-accounts
- Skills and resources index: https://zkcompression.com/skill.md
- Crate: light-sdk (LightAccount, CpiAccounts, LightSystemProgramCpi, derive_light_cpi_signer!)
- Anchor example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/update
- Native Rust example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/update
Key SDK API: LightAccount::new_mut()
### 1. Index project
- Grep `declare_id|#\[program\]|entrypoint!|Pubkey|AccountInfo|mut|update|modify` across src/
- Glob `**/*.rs` and `**/Cargo.toml` for project structure
- Identify: program ID, existing instructions, account structs, framework (Anchor or native)
- Read Cargo.toml — note existing dependencies and Solana SDK version
- Task subagent (Grep/Read/WebFetch) if project has multiple crates to scan in parallel
### 2. Read references
- WebFetch the guide above — review both Anchor and Native Rust code samples
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: what is the goal? (new program from scratch, add account update to existing program, migrate from regular accounts)
- AskUserQuestion: Anchor or Native Rust framework?
- AskUserQuestion: does the program already have compressed account instructions, or is this the first one?
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan: which files to modify, what code to add, dependency changes
- Follow the guide's step order: Dependencies → Constants → Account Struct → Instruction Data → Update Compressed Account → Light System Program CPI
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- Add deps: Bash `cargo add light-sdk@0.23` (add `anchor_lang@0.31` for Anchor or `solana-program@2.2` + `borsh@0.10` for Native Rust)
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- Bash `cargo build-sbf` or `anchor build`
- Bash `cargo test-sbf` or `anchor test` if tests exist
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work
```
Copy the prompt below or view the [guide](/compressed-pdas/guides/how-to-close-compressed-accounts).
```text theme={null}
---
description: Build a program that closes compressed accounts
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Build a program that closes compressed accounts
Context:
- Guide: https://zkcompression.com/compressed-pdas/guides/how-to-close-compressed-accounts
- Skills and resources index: https://zkcompression.com/skill.md
- Crate: light-sdk (LightAccount, CpiAccounts, LightSystemProgramCpi, derive_light_cpi_signer!)
- Anchor example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/close
- Native Rust example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/close
Key SDK API: LightAccount::new_close()
### 1. Index project
- Grep `declare_id|#\[program\]|entrypoint!|Pubkey|AccountInfo|close|delete|remove` across src/
- Glob `**/*.rs` and `**/Cargo.toml` for project structure
- Identify: program ID, existing instructions, account structs, framework (Anchor or native)
- Read Cargo.toml — note existing dependencies and Solana SDK version
- Task subagent (Grep/Read/WebFetch) if project has multiple crates to scan in parallel
### 2. Read references
- WebFetch the guide above — review both Anchor and Native Rust code samples
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: what is the goal? (new program from scratch, add account close to existing program, migrate from regular accounts)
- AskUserQuestion: Anchor or Native Rust framework?
- AskUserQuestion: does the program already have compressed account instructions, or is this the first one?
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan: which files to modify, what code to add, dependency changes
- Follow the guide's step order: Dependencies → Constants → Account Struct → Instruction Data → Close Compressed Account → Light System Program CPI
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- Add deps: Bash `cargo add light-sdk@0.23` (add `anchor_lang@0.31` for Anchor or `solana-program@2.2` + `borsh@0.10` for Native Rust)
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- Bash `cargo build-sbf` or `anchor build`
- Bash `cargo test-sbf` or `anchor test` if tests exist
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work
```
Copy the prompt below or view the [guide](/compressed-pdas/guides/how-to-reinitialize-compressed-accounts).
```text theme={null}
---
description: Build a program that reinitializes closed compressed accounts
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Build a program that reinitializes closed compressed accounts
Context:
- Guide: https://zkcompression.com/compressed-pdas/guides/how-to-reinitialize-compressed-accounts
- Skills and resources index: https://zkcompression.com/skill.md
- Crate: light-sdk (LightAccount, CpiAccounts, LightSystemProgramCpi, derive_light_cpi_signer!)
- Anchor example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/reinit
- Native Rust example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/reinit
Key SDK API: LightAccount::new_empty()
### 1. Index project
- Grep `declare_id|#\[program\]|entrypoint!|Pubkey|AccountInfo|reinit|empty|reset` across src/
- Glob `**/*.rs` and `**/Cargo.toml` for project structure
- Identify: program ID, existing instructions, account structs, framework (Anchor or native)
- Read Cargo.toml — note existing dependencies and Solana SDK version
- Task subagent (Grep/Read/WebFetch) if project has multiple crates to scan in parallel
### 2. Read references
- WebFetch the guide above — review both Anchor and Native Rust code samples
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: what is the goal? (new program from scratch, add account reinitialization to existing program, migrate from regular accounts)
- AskUserQuestion: Anchor or Native Rust framework?
- AskUserQuestion: does the program already have compressed account instructions, or is this the first one?
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan: which files to modify, what code to add, dependency changes
- Follow the guide's step order: Dependencies → Constants → Account Struct → Instruction Data → Reinitialize Closed Account → Light System Program CPI
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- Add deps: Bash `cargo add light-sdk@0.23` (add `anchor_lang@0.31` for Anchor or `solana-program@2.2` + `borsh@0.10` for Native Rust)
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- Bash `cargo build-sbf` or `anchor build`
- Bash `cargo test-sbf` or `anchor test` if tests exist
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work
```
Copy the prompt below or view the [guide](/compressed-pdas/guides/how-to-burn-compressed-accounts).
```text theme={null}
---
description: Build a program that permanently burns compressed accounts
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Build a program that permanently burns compressed accounts
Context:
- Guide: https://zkcompression.com/compressed-pdas/guides/how-to-burn-compressed-accounts
- Skills and resources index: https://zkcompression.com/skill.md
- Crate: light-sdk (LightAccount, CpiAccounts, LightSystemProgramCpi, derive_light_cpi_signer!)
- Anchor example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/burn
- Native Rust example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/burn
Key SDK API: LightAccount::new_burn()
### 1. Index project
- Grep `declare_id|#\[program\]|entrypoint!|Pubkey|AccountInfo|burn|destroy|permanent` across src/
- Glob `**/*.rs` and `**/Cargo.toml` for project structure
- Identify: program ID, existing instructions, account structs, framework (Anchor or native)
- Read Cargo.toml — note existing dependencies and Solana SDK version
- Task subagent (Grep/Read/WebFetch) if project has multiple crates to scan in parallel
### 2. Read references
- WebFetch the guide above — review both Anchor and Native Rust code samples
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: what is the goal? (new program from scratch, add account burn to existing program, migrate from regular accounts)
- AskUserQuestion: Anchor or Native Rust framework?
- AskUserQuestion: does the program already have compressed account instructions, or is this the first one?
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan: which files to modify, what code to add, dependency changes
- Follow the guide's step order: Dependencies → Constants → Account Struct → Instruction Data → Burn Compressed Account → Light System Program CPI
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- Add deps: Bash `cargo add light-sdk@0.23` (add `anchor_lang@0.31` for Anchor or `solana-program@2.2` + `borsh@0.10` for Native Rust)
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- Bash `cargo build-sbf` or `anchor build`
- Bash `cargo test-sbf` or `anchor test` if tests exist
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work
```
Copy the prompt below or view the [guide](/resources/migration-v1-to-v2).
```text theme={null}
---
argument-hint:
description: Migrate Light Protocol program from v1 to v2 Merkle trees
allowed-tools: [Bash, Read, Glob, Grep, Task, WebFetch]
---
Migrate this Light Protocol program from v1 to v2 Merkle trees.
## Goal
Produce a **fully working migration** that builds and tests pass.
## Available commands
Via Bash tool:
- **cargo build-sbf**, **cargo test-sbf**, **cargo fmt**, **cargo clippy**
- **anchor build**, **anchor test**
- **grep**, **sed**
## Documentation
- Migration Guide: https://zkcompression.com/references/migration-v1-to-v2
- Reference PR: https://github.com/Lightprotocol/program-examples/commit/54f0e7f15c2972a078f776cfb40b238d83c7e486
## Reference repos
program-examples/counter/anchor/
├── programs/counter/src/lib.rs # v2 patterns: derive_address, CpiAccounts
├── Cargo.toml # v2 feature flags
└── tests/counter.ts # v2 client patterns
## Workflow
### Phase 1: Index program
Find all v1 patterns:
grep -r "::v1::" src/ tests/
grep -r "ADDRESS_TREE_V1" src/
grep -r "into_new_address_params_packed" src/
grep -r "get_address_tree_v1" tests/
### Phase 2: Update dependencies
Update Cargo.toml. V2 is the default - no feature flag needed:
# On-chain program
[dependencies]
light-sdk = { version = "0.17.1", features = ["anchor"] }
light-hasher = "5.0.0"
# Off-chain client
[dependencies]
light-client = "0.17.2"
Note: V2 is now the default in all crates. Only specify `features = ["v2"]` if you disabled default features.
### Phase 3: Rust SDK replacements
| v1 Pattern | v2 Replacement |
|------------|----------------|
| address::v1::derive_address | address::v2::derive_address |
| cpi::v1::CpiAccounts | cpi::v2::CpiAccounts |
| cpi::v1::LightSystemProgramCpi | cpi::v2::LightSystemProgramCpi |
| constants::ADDRESS_TREE_V1 | constants::ADDRESS_TREE_V2 |
| .into_new_address_params_packed(seed) | .into_new_address_params_assigned_packed(seed, Some(0)) |
| .add_system_accounts(config) | .add_system_accounts_v2(config) |
### Phase 4: TypeScript SDK replacements
| v1 Pattern | v2 Replacement |
|------------|----------------|
| deriveAddress( | deriveAddressV2( |
| deriveAddressSeed( | deriveAddressSeedV2( |
| defaultTestStateTreeAccounts().addressTree | batchAddressTree |
| .newWithSystemAccounts( | .newWithSystemAccountsV2( |
| get_address_tree_v1() | get_address_tree_v2() |
| get_random_state_tree_info_v1() | get_random_state_tree_info() |
### Phase 5: Build and test loop
**Required commands (no shortcuts):**
For Anchor programs: **anchor build && anchor test**
For Native programs: **cargo build-sbf && cargo test-sbf**
**NO shortcuts allowed:**
- Do NOT use **cargo build** (must use **cargo build-sbf**)
- Do NOT use **cargo test** (must use **cargo test-sbf**)
- Tests MUST run against real BPF bytecode
**On failure:** Spawn debugger agent with error context.
**Loop rules:**
1. Each debugger gets fresh context + previous debug reports
2. Each attempt tries something DIFFERENT
3. **NEVER GIVE UP** - keep spawning until fixed
Do NOT proceed until all tests pass.
## DeepWiki fallback
If no matching pattern in reference repos:
mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "How to migrate {pattern} from v1 to v2?")
```
# Getcompressedaccount
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getcompressedaccount
post /getCompressedAccount
Retrieve compressed account information by address or hash. RPC method guide with use cases, tips and examples.
# Getcompressedaccountsbyowner
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getcompressedaccountsbyowner
post /getCompressedAccountsByOwner
Retrieve all compressed accounts owned by a specific address. RPC method guide with use cases, tips and examples.
# Getcompressedbalance
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getcompressedbalance
post /getCompressedBalance
Retrieve the lamport balance for a specific compressed account by address or hash. Overview with RPC method guide.
# Getcompressedbalancebyowner
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getcompressedbalancebyowner
post /getCompressedBalanceByOwner
Query the total compressed token balance for a specific account owner. Overview with RPC method guide.
# Getcompressedminttokenholders
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getcompressedminttokenholders
post /getCompressedMintTokenHolders
Retrieve owner balances for a given mint in descending order. Overview with RPC method guide.
# Getcompressedtokenaccountbalance
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getcompressedtokenaccountbalance
post /getCompressedTokenAccountBalance
Retrieve the balance for a given token account by address or hash. Overview with RPC method guide
# Getcompressedtokenaccountbydelegate
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getcompressedtokenaccountbydelegate
post /getCompressedTokenAccountsByDelegate
Retrieve compressed token accounts that are partially or fully delegated to a given delegate. Overview with RPC method guide.
# Getcompressedtokenaccountsbyowner
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getcompressedtokenaccountsbyowner
post /getCompressedTokenAccountsByOwner
Retrieve compressed token accounts owned by a specific address. RPC method guide with use cases, tips and examples.
# Getcompressedtokenbalancesbyowner
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getcompressedtokenbalancesbyowner
post /getCompressedTokenBalancesByOwner
Retrieves all token balances for compressed accounts owned by an address. Overview with RPC method guide.
# Getcompressionsignaturesforaccount
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getcompressionsignaturesforaccount
post /getCompressionSignaturesForAccount
Retrieve the signatures of the transactions that closed or opened a compressed account with the given hash. Overview with RPC method guide.
# Getcompressionsignaturesforaddress
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getcompressionsignaturesforaddress
post /getCompressionSignaturesForAddress
Retrieve the signatures of the transactions that closed or opened a compressed account with the given address. Overview with RPC method guide.
# Getcompressionsignaturesforowner
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getcompressionsignaturesforowner
post /getCompressionSignaturesForOwner
Retrieve the signatures of the transactions that have modified an owner's compressed accounts. Overview with RPC method guide.
# Getcompressionsignaturesfortokenowner
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getcompressionsignaturesfortokenowner
post /getCompressionSignaturesForTokenOwner
Retrieve the signatures of the transactions that have modified an owner's compressed token accounts. Overview with RPC method guide.
# Getindexerhealth
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getindexerhealth
post /getIndexerHealth
Retrieve an error if the indexer is stale by more than a configurable number of blocks. Otherwise, it returns ok. Overview with RPC method guide.
# Getindexerslot
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getindexerslot
post /getIndexerSlot
Retrieve the slot of the last block indexed by the indexer. Overview with RPC method guide.
# Getlatestcompressionsignatures
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getlatestcompressionsignatures
post /getLatestCompressionSignatures
Retrieve the signatures of the latest transactions that used the compression program. This is a paginated endpoint. Overview with RPC method guide.
# Getlatestnonvotingsignatures
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getlatestnonvotingsignatures
post /getLatestNonVotingSignatures
Retrieve the signatures of the latest transactions that are not voting transactions. Overview with RPC method guide.
# Getmultiplecompressedaccounts
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getmultiplecompressedaccounts
post /getMultipleCompressedAccounts
Retrieve multiple compressed accounts with the given addresses or hashes. Overview with RPC method guide.
# Getmultiplenewaddressproofs
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getmultiplenewaddressproofs
post /getMultipleNewAddressProofs
Retrieve proofs that the new addresses are not taken already and can be created. Overview with RPC method guide.
# Gettransactionwithcompressioninfo
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/gettransactionwithcompressioninfo
post /getTransactionWithCompressionInfo
Retrieve the transaction data for the transaction with the given signature along with parsed compression info. RPC method guide with use cases, tips and examples.
The`getTransactionWithCompressionInfo` RPC method returns transaction data along with compression information showing which compressed accounts were opened (created) and closed (consumed) during the transaction. This method helps with transaction analysis, account lifecycle tracking, and debugging of compression operations.
You can test this method via the OpenAPI example or custom examples below.
**Common Use Cases**
* **Transaction Analysis**: Understand what compressed accounts were affected by a transaction
* **Account Lifecycle Tracking**: Monitor when compressed accounts are created and consumed
* **Debugging**: Identify which accounts changed during failed or unexpected transactions
* **Audit Trails**: Track compressed account state changes for compliance
**Parameters**
1. `signature` (string, required): Base58-encoded transaction signature to query compression information for.
**Note**: Only transactions involving compressed accounts will return compression data. Regular Solana transactions return null.
**Response**
The response contains compression information and transaction data, or null if transaction not found:
* `compressionInfo` (object): Contains details about compressed account changes
* `closedAccounts` (array): Compressed accounts consumed (spent) in this transaction
* `account` (object): Complete compressed account data with merkle context
* `maybeTokenData` (object | null): Token data if this is a compressed token account
* `openedAccounts` (array): New compressed accounts created in this transaction
* `account` (object): Complete compressed account data with merkle context
* `maybeTokenData` (object | null): Token data if this is a compressed token account
* `preTokenBalances` (array, optional): Token balances before transaction
* `owner` (PublicKey): Public key of token account owner
* `mint` (PublicKey): Public key of token mint
* `amount` (BN): Token amount as BN object
* `postTokenBalances` (array, optional): Token balances after transaction
* `owner` (PublicKey): Public key of token account owner
* `mint` (PublicKey): Public key of token mint
* `amount` (BN): Token amount as BN object
* `transaction` (object): Standard Solana transaction data
**Developer Tips**
* **Compression-only**: This method only works with transactions that involve compressed accounts
* **Real signatures required**: Use actual transaction signatures from compression operations
* **Account lifecycle**: opened = created, closed = consumed/spent in the transaction
* **Token data**: maybeTokenData is null for regular compressed accounts, populated for token accounts
* **Balance tracking**: Use pre/postTokenBalances for detailed token amount changes
* **State analysis**: Compare opened vs closed accounts to understand transaction effects
**Troubleshooting**
**Invalid or non-existent transaction signature**
Verify the signature format and check transaction existence:
```typescript theme={null}
const result = await connection.getTransactionWithCompressionInfo(signature);
if (!result) {
console.log('Transaction not found or contains no compression operations');
}
```
**Transaction exists but has no compression data**
This method only returns data for transactions involving compressed accounts:
```typescript theme={null}
const result = await connection.getTransactionWithCompressionInfo(signature);
if (!result) {
console.log('Transaction does not involve compressed accounts');
console.log('Use regular getTransaction for non-compression transactions');
}
```
**Transaction has compression info but no account changes shown**
Some compression operations may not create/consume accounts:
```typescript theme={null}
const { compressionInfo } = result;
if (compressionInfo.openedAccounts.length === 0 &&
compressionInfo.closedAccounts.length === 0) {
console.log('Compression transaction with no visible account changes');
console.log('May be a proof verification or state update operation');
}
```
# Getvalidityproof
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/getvalidityproof
post /
Retrieve a single ZK Proof used by the compression program to verify that the given accounts are valid and the new addresses can be created. RPC method guide with use cases, tips and examples.
The`getValidityProof` RPC method generates zero-knowledge proofs to verify that the given accounts are valid or the new addresses can be created. This proof is required for any operation on compressed accounts (transfer, approve, decompress, etc.) for on-chain verification of compressed state.
You can test this method via the OpenAPI example or custom examples below.
* Proof limits per request are:
* `hashes`: 1, 2, 3, 4, or 8
* `newAddressesWithTrees` : 1, 2
* The `newAddresses` param field is supported but deprecated. Please use `newAddressesWithTrees`instead.
**Common Use Cases**
* **Token Transfers:** Generate proofs required for transferring compressed tokens.
* **Account Operations:** Create proofs needed for any compressed account modification.
* **Batch Processing:** Generate proofs for multiple accounts in a single call.
* **State Tree Verification:** Prove account inclusion in the current state tree.
* **Transaction Building:** Obtain proof data needed for compressed transaction instructions.
* **Program Integration:** Get validity proofs for custom program operations on compressed accounts.
**Parameters**
1. `hashes` (BN254\[], required): Array of BN254 objects representing compressed account hashes to generate proofs for.
2. `newAddresses` (BN254\[], optional): Array of BN254 objects representing new addresses to include in the proof for address tree verification.
**Response**
The response contains ValidityProofWithContext data:
* `compressedProof` (ValidityProof | null): The compressed validity proof object for zero-knowledge verification
* `roots` (BN\[]): Array of merkle tree roots used in proof generation
* `rootIndices` (number\[]): Array of indices of the roots in the state tree
* `leafIndices` (number\[]): Array of indices of the leaves being proven
* `leaves` (BN\[]): Array of leaf values in the merkle tree
* `treeInfos` (TreeInfo\[]): Array of information about the state trees used
* `proveByIndices` (boolean\[]): Array indicating whether to prove by indices for each element
**Developer Tips**
* **Fetch Accounts First**: Always get compressed accounts before generating proofs - you need existing account hashes
* **Batch Processing**: You can generate proofs for multiple accounts in a single call. Allowed counts: 1, 2, 3, 4, or 8 accounts
* **Proof Usage**: The proof is required for any operation that modifies compressed accounts (transfers, burns, etc.)
* **Caching**: Validity proofs are only valid for the current state - don't cache them for long periods
* **Error Handling**: If accounts don't exist or have been modified, proof generation will fail
* **Integration**: Use `proof.compressedProof` and `proof.rootIndices` when building transactions
* **State Changes**: After any transaction, previously generated proofs become invalid for those accounts
**Examples**
The below examples work - just make sure you installed the dependencies.
```bash theme={null}
npm install @lightprotocol/stateless.js @solana/web3.js
```
**Example: Generate Validity Proof**
Generate a validity proof for compressed accounts.
```typescript expandable theme={null}
import { Rpc, createRpc, bn } from '@lightprotocol/stateless.js';
import { PublicKey } from '@solana/web3.js';
async function generateValidityProof(): Promise {
const connection: Rpc = createRpc(
'https://devnet.helius-rpc.com/?api-key=YOUR_API_KEY',
'https://devnet.helius-rpc.com/?api-key=YOUR_API_KEY'
);
try {
// Get compressed accounts
const owner = new PublicKey('OWNER_PUBLIC_KEY_HERE');
const accounts = await connection.getCompressedAccountsByOwner(owner);
if (accounts.items.length === 0) {
console.log('No compressed accounts found');
return;
}
// Generate validity proof
const hashes = accounts.items.slice(0, 2).map(acc => bn(acc.hash));
const validityProof = await connection.getValidityProof(hashes);
console.log('Validity Proof Generated:');
console.log(\` Accounts: ${hashes.length}\`);
console.log(\` Roots: ${validityProof.roots.length}\`);
console.log(\` Root Indices: ${validityProof.rootIndices.length} elements\`);
console.log(\` Leaf Indices: ${validityProof.leafIndices.length} elements\`);
// Ready for use in transactions
console.log('\nProof ready for compressed transactions');
} catch (error: unknown) {
console.error('Error generating validity proof:', error);
}
}
generateValidityProof();
```
```bash theme={null}
# Use account hashes from getCompressedAccountsByOwner
curl -X POST https://devnet.helius-rpc.com/?api-key=YOUR_API_KEY \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "getValidityProof",
"params": {
"hashes": [
"ACCOUNT_HASH_1",
"ACCOUNT_HASH_2"
]
}
}'
```
**Rust Client**: `light-client` v0.14.0 is available on crates.io. Use `LightClient` for the current stable API.
```rust expandable theme={null}
use light_client::rpc::LightClient;
use solana_sdk::pubkey::Pubkey;
use std::str::FromStr;
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let client = LightClient::new("https://devnet.helius-rpc.com/?api-key=YOUR_API_KEY".to_string()).await?;
// Get compressed accounts first
let owner = "OWNER_PUBLIC_KEY_HERE";
let accounts = client.get_compressed_accounts_by_owner(owner).await?;
if accounts.items.is_empty() {
println!("No compressed accounts found");
return Ok(());
}
// Generate validity proof for the accounts
let hashes: Vec = accounts.items.iter()
.take(2)
.map(|acc| acc.hash.clone())
.collect();
let validity_proof = client.get_validity_proof(hashes.clone()).await?;
println!("Validity Proof Generated:");
println!(" Accounts: {}", hashes.len());
println!(" Roots: {}", validity_proof.roots.len());
println!(" Root Indices: {}", validity_proof.root_indices.len());
Ok(())
}
```
**Example: Proof Generation for Token Transfer**
Generate a validity proof for a compressed token transfer operation:
```Javascript JavaScript expandable theme={null}
import { Rpc, createRpc, bn } from '@lightprotocol/stateless.js';
import { PublicKey } from '@solana/web3.js';
async function generateProofForTransfer() {
const connection = createRpc(
'https://devnet.helius-rpc.com/?api-key=YOUR_API_KEY',
'https://devnet.helius-rpc.com/?api-key=YOUR_API_KEY'
);
try {
// Get compressed token accounts
const owner = new PublicKey('OWNER_PUBLIC_KEY_HERE');
const mint = new PublicKey('TOKEN_MINT_HERE');
const tokenAccounts = await connection.getCompressedTokenAccountsByOwner(
owner,
{ mint }
);
if (tokenAccounts.items.length === 0) {
console.log('No compressed token accounts found');
return;
}
// Generate proof for the token accounts (limit to allowed numbers: 1, 2, 3, 4, or 8)
const limitedAccounts = tokenAccounts.items.slice(0, 4);
const hashes = limitedAccounts.map(acc => bn(acc.compressedAccount.hash));
const validityProof = await connection.getValidityProof(hashes);
console.log('Validity Proof for Transfer:');
console.log(\` Token accounts: ${hashes.length}\`);
console.log(\` Roots: ${validityProof.roots.length}\`);
// This proof is now ready to use with LightTokenProgram.transfer()
console.log('\nProof ready for token transfer instruction');
console.log('Use this with:');
console.log(' recentValidityProof: validityProof.compressedProof');
console.log(' recentInputStateRootIndices: validityProof.rootIndices');
} catch (error) {
console.error('Error generating proof:', error);
}
}
generateProofForTransfer();
```
# JSON RPC Methods
Source: https://www.zkcompression.com/api-reference/json-rpc-methods/overview
Photon indexer methods for querying compressed state on Solana.
The compressed token SDK calls these methods internally via interface functions
like `transferInterface` and `getAtaInterface`. You rarely need to call
them directly.
Call these methods directly if you are building a block explorer,
custom indexer, or working with
[legacy compressed tokens](/resources/legacy-compressed-tokens).
| Solana RPC | Photon RPC |
| :---------------------- | :--------------------------------- |
| getAccountInfo | getCompressedAccount |
| getBalance | getCompressedBalanceByOwner |
| getAccountInfo | getCompressedTokenAccountsByOwner |
| getTokenAccountBalance | getCompressedTokenBalancesByOwner |
| getSignaturesForAddress | getCompressionSignaturesForAddress |
| getSignaturesForAddress | getCompressionSignaturesForOwner |
## Create an RPC connection
```typescript theme={null}
// Helius exposes Solana and Photon RPC endpoints through a single URL
import { createRpc, Rpc } from '@lightprotocol/stateless.js';
const RPC_ENDPOINT = 'https://mainnet.helius-rpc.com?api-key=YOUR_KEY';
const connection: Rpc = createRpc(RPC_ENDPOINT, RPC_ENDPOINT);
```
```typescript theme={null}
import { createRpc, Rpc } from '@lightprotocol/stateless.js';
// Helius exposes Solana and Photon RPC endpoints through a single URL
const RPC_ENDPOINT = "https://devnet.helius-rpc.com?api-key=";
const connection: Rpc = createRpc(RPC_ENDPOINT, RPC_ENDPOINT);
```
## Best practices
| Best practice | Description |
| :-------------------- | :------------------------------------------------------------------------------------------------------------- |
| **Commitment levels** | Use appropriate commitment levels: `processed` (fastest), `confirmed` (balanced), `finalized` (most reliable). |
| **Rate limiting** | Implement retry logic and respect rate limits. Public endpoints: 100 req/s, Private: 1000+ req/s. |
| **Batch requests** | Use batch requests when possible to reduce API calls. |
| **Caching** | Cache frequently accessed data to reduce API calls. |
## Error codes
| Code | Message | Description |
| :----- | :------------------- | :-------------------------------------------- |
| -32600 | Invalid Request | The JSON sent is not a valid Request object. |
| -32601 | Method not found | The method does not exist / is not available. |
| -32602 | Invalid params | Invalid method parameter(s). |
| -32603 | Internal error | Internal JSON-RPC error. |
| -32000 | Account not found | The compressed account was not found. |
| -32001 | Invalid account hash | The provided account hash is invalid. |
## Methods
These methods are called internally by the compressed token SDK. Call them
directly for custom indexing, block explorers, or debugging.
|
|
| [GetCompressedAccount](/api-reference/json-rpc-methods/getcompressedaccount) |
Returns a compressed account by address or hash. |
| [GetCompressedAccountsByOwner](/api-reference/json-rpc-methods/getcompressedaccountsbyowner) |
Returns all compressed accounts for an owner. |
| [GetCompressedBalanceByOwner](/api-reference/json-rpc-methods/getcompressedbalancebyowner) |
Returns the total compressed balance for an owner. |
| [GetCompressedTokenAccountsByOwner](/api-reference/json-rpc-methods/getcompressedtokenaccountsbyowner) |
Returns all compressed token accounts for an owner. |
| [GetCompressedTokenBalancesByOwner](/api-reference/json-rpc-methods/getcompressedtokenbalancesbyowner) |
Returns all token balances for compressed accounts owned by an address. |
| [GetValidityProof](/api-reference/json-rpc-methods/getvalidityproof) |
Returns a validity proof for compressed state transitions. |
| [GetCompressionSignaturesForAddress](/api-reference/json-rpc-methods/getcompressionsignaturesforaddress) |
Returns signatures for transactions involving an address. |
| [GetCompressionSignaturesForOwner](/api-reference/json-rpc-methods/getcompressionsignaturesforowner) |
Returns signatures for transactions where an address is the owner. |
| [GetTransactionWithCompressionInfo](/api-reference/json-rpc-methods/gettransactionwithcompressioninfo) |
Returns transaction details with compression context. |
| [GetIndexerHealth](/api-reference/json-rpc-methods/getindexerhealth) |
Returns the health status of the Photon indexer. |
| [GetIndexerSlot](/api-reference/json-rpc-methods/getindexerslot) |
Returns the current slot of the Photon indexer. |
## Legacy
Methods for direct compressed-token workflows. Not called by the Light
Token SDK interface functions.
|
|
| [GetCompressedBalance](/api-reference/json-rpc-methods/getcompressedbalance) |
Returns the balance of a single compressed account. |
| [GetCompressedMintTokenHolders](/api-reference/json-rpc-methods/getcompressedminttokenholders) |
Lists all holders of a compressed token mint. |
| [GetCompressedTokenAccountBalance](/api-reference/json-rpc-methods/getcompressedtokenaccountbalance) |
Returns the token balance of a single compressed token account. |
| [GetCompressedTokenAccountsByDelegate](/api-reference/json-rpc-methods/getcompressedtokenaccountbydelegate) |
Returns all compressed token accounts delegated to an address. |
| [GetCompressionSignaturesForAccount](/api-reference/json-rpc-methods/getcompressionsignaturesforaccount) |
Returns signatures for transactions involving a compressed account. |
| [GetCompressionSignaturesForTokenOwner](/api-reference/json-rpc-methods/getcompressionsignaturesfortokenowner) |
Returns signatures for token transactions by owner. |
| [GetLatestCompressionSignatures](/api-reference/json-rpc-methods/getlatestcompressionsignatures) |
Returns the most recent compression-related signatures. |
| [GetLatestNonVotingSignatures](/api-reference/json-rpc-methods/getlatestnonvotingsignatures) |
Returns recent non-voting transaction signatures. |
| [GetMultipleCompressedAccounts](/api-reference/json-rpc-methods/getmultiplecompressedaccounts) |
Returns multiple compressed accounts in a single request. |
| [GetMultipleNewAddressProofs](/api-reference/json-rpc-methods/getmultiplenewaddressproofs) |
Returns proofs that new addresses can be created. |
# SDK Reference
Source: https://www.zkcompression.com/api-reference/sdk
Client and program SDKs for Compressed Tokens and ZK Compression.
### Client SDKs
TypeScript RPC client for ZK Compression and compressed accounts.
Rust RPC client for ZK Compression.
TypeScript token operations for compressed tokens.
### Program
Core SDK for on-chain programs.
Procedural macros for Light accounts.
Local testing framework for programs.
# Overview
Source: https://www.zkcompression.com/compressed-pdas/guides
Overview to guides for Solana programs to create, update, close, reinitialize, and burn permanently compressed accounts.
|
|
|
Create
|
Initialize compressed PDAs in your program |
|
Update
|
Modify state in compressed accounts |
|
Close
|
Reclaim lamports from compressed accounts |
|
Reinitialize
|
Reset and reuse compressed accounts |
|
Burn
|
Permanently delete compressed accounts |
|
Nullifier PDAs
|
Prevent replay attacks with one-time use accounts |
Here is the complete flow to create, or interact with compressed accounts:
## Next Steps
# Client Guide
Source: https://www.zkcompression.com/compressed-pdas/guides/client-guide
Rust and Typescript client guides with step-by-step implementation and full code examples.
|
|
|
| **TypeScript** |
[@lightprotocol/stateless.js](https://lightprotocol.github.io/light-protocol/stateless.js/index.html) |
Client SDK for Compressed Accounts |
| **TypeScript** |
[@lightprotocol/compressed-token](https://lightprotocol.github.io/light-protocol/compressed-token/index.html) |
Client SDK for Compressed Tokens |
| **Rust** |
[light-client](https://docs.rs/light-client) |
Client 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 Light System Program and Merkle tree accounts. `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.
# Get Started
## Setup
Use the [API documentation](https://lightprotocol.github.io/light-protocol/) to look up specific function signatures, parameters, and return types.
### 1. Installation
```bash theme={null}
npm install --save \
@lightprotocol/stateless.js \
@lightprotocol/compressed-token \
@solana/web3.js
```
```bash theme={null}
yarn add \
@lightprotocol/stateless.js \
@lightprotocol/compressed-token \
@solana/web3.js
```
```bash theme={null}
pnpm add \
@lightprotocol/stateless.js \
@lightprotocol/compressed-token \
@solana/web3.js
```
### 2. RPC Connection
`Rpc` is a thin wrapper extending Solana's web3.js `Connection` class with compression-related endpoints.
```typescript theme={null}
const rpc = createRpc('https://mainnet.helius-rpc.com/?api-key=YOUR_API_KEY');
```
```typescript theme={null}
const rpc = createRpc('https://devnet.helius-rpc.com/?api-key=YOUR_API_KEY');
```
1. Install the CLI
```bash theme={null}
npm i -g @lightprotocol/zk-compression-cli
```
2. Start a local Solana test validator, photon indexer, and prover server on default ports 8899, 8784, and 3001.
```bash theme={null}
light test-validator
```
### 1. Dependencies
```toml theme={null}
[dependencies]
light-client = "0.23.0"
light-sdk = "0.23.0"
```
### 2. RPC Connection
Connect to an RPC provider that supports ZK Compression, such as Helius and Triton.
```rust theme={null}
let config = LightClientConfig::new(
"https://api.mainnet-beta.solana.com".to_string(),
Some("https://mainnet.helius.xyz".to_string()),
Some("YOUR_API_KEY".to_string())
);
let mut client = LightClient::new(config).await?;
client.payer = read_keypair_file("~/.config/solana/id.json")?;
```
```rust theme={null}
let config = LightClientConfig::devnet(
Some("https://devnet.helius-rpc.com".to_string()),
Some("YOUR_API_KEY".to_string())
);
let mut client = LightClient::new(config).await?;
client.payer = read_keypair_file("~/.config/solana/id.json")?;
```
```rust theme={null}
let config = LightClientConfig::local();
let mut client = LightClient::new(config).await?;
client.payer = read_keypair_file("~/.config/solana/id.json")?;
```
1. Install the CLI
```bash theme={null}
npm i -g @lightprotocol/zk-compression-cli
```
2. Start a single-node Solana cluster, an RPC node, and a prover node at ports 8899, 8784, and 3001.
```bash theme={null}
light test-validator
```
## Address
Derive a persistent address as a unique identifier for your compressed account, similar to [program-derived addresses (PDAs)](https://solana.com/docs/core/pda).
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 theme={null}
const addressTree = await rpc.getAddressTreeInfoV2();
const seed = deriveAddressSeedV2(
[Buffer.from('my-seed')]
);
const address = deriveAddressV2(
seed,
addressTree.tree,
programId
);
```
```rust theme={null}
use light_sdk::address::v2::derive_address;
let address_tree_info = rpc.get_address_tree_v2();
let (address, _) = derive_address(
&[b"my-seed"],
&address_tree_info.tree,
&program_id,
);
```
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](https://www.zkcompression.com/resources/addresses-and-urls).
## 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 theme={null}
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.
```typescript theme={null}
const proof = await rpc.getValidityProofV0(
[{
hash: compressedAccount.hash,
tree: compressedAccount.treeInfo.tree,
queue: compressedAccount.treeInfo.queue
}],
[]
);
```
**1. Pass these parameters**:
Specify the **account hash**, `tree` and `queue` pubkeys from the compressed account's `TreeInfo`.
* You don't specify the address for update, close, reinitialize, and burn instructions.
* The proof **verifies the account hash exists in the state tree** for these instructions.
* The validity proof structure is identical. The difference is in your program's instruction handler.
**2. The RPC returns**:
* The proof that the account hash exists in the state tree for your instruction data.
* `rootIndices` and `leafIndices` arrays with proof metadata to pack accounts.
```rust theme={null}
let rpc_result = rpc
.get_validity_proof(
vec![],
vec![AddressWithTree {
address: *address,
tree: address_tree_info.tree
}],
None,
)
.await?
.value;
```
**1. Pass these parameters**:
* **Specify the new address** and `tree` pubkey from the address tree `TreeInfo`. The `queue` pubkey is only required in TypeScript.
* When you create an account you don't reference a compressed account hash in the hash array (`vec![]`).
For account creation, you prove the address does not exist yet in the address tree.
**2. The RPC returns `ValidityProofWithContext`**:
* The proof that the new address does not exist in the address tree for your instruction data.
* `addresses` with the public key and metadata of the address tree to pack accounts.
```rust theme={null}
let rpc_result = rpc
.get_validity_proof(
vec![compressed_account.hash],
vec![],
None,
)
.await?
.value;
```
**1. Pass these parameters**:
Specify the **account hash**, `tree` and `queue` pubkeys from the compressed account's `TreeInfo`.
* You don't specify the address for update, close, reinitialize, and burn instructions.
* The proof **verifies the account hash exists in the state tree** for these instructions.
* The validity proof structure is identical. The difference is in your program's instruction handler.
**2. The RPC returns `ValidityProofWithContext`**:
* The proof that the **account hash exists in the state tree** for your instruction data
* `accounts` with the **public key and metadata of the state tree** to pack accounts.
### Optimize with Combined Proofs
You can prove **in a single proof**:
* multiple addresses,
* multiple account hashes, or
* a combination of addresses and account hashes.
| | |
| ---------------------- | ----------------------------------------------------------------------------------------- |
| Account Hash-only | 1 to 8 hashes |
| Address-only | 1 to 8 addresses |
| Mixed (hash + address) | Any combination of
**1 to 4** account hashes **and**
**1 or 4** 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 theme={null}
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.
```rust theme={null}
let rpc_result = rpc
.get_validity_proof(
vec![compressed_account.hash],
vec![AddressWithTree {
address: *address,
tree: address_tree_info.tree
}],
None,
)
.await?
.value;
```
**1. Pass these parameters**:
* Specify one or more **compressed account hashes**.
* Specify one or more **derived addresses** with their `tree` pubkeys from the address tree `TreeInfo`. The `queue` pubkey is only required in TypeScript.
**2. The RPC returns `ValidityProofWithContext`**:
* A single combined proof that verifies both the **account hash exists in the state tree** and the **new address does not exist in the address tree** for your instruction data
* New `addresses` with the public key and metadata of the address tree to pack accounts.
* `accounts` with the public key and metadata of the state tree to pack accounts.
See the full [create-and-update program example for this proof combination with tests](https://github.com/Lightprotocol/program-examples/tree/main/create-and-update).
## Accounts
To interact with a compressed account you need System accounts such as the Light System Program,
and Merkle tree accounts.
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.
|
|
|
| 1 |
Light System Program |
Verifies validity proofs, compressed account ownership checks, and CPIs the Account Compression Program to update tree accounts. |
| 2 |
CPI 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.
|
| 3 |
Registered Program PDA |
Provides access control to the Account Compression Program. |
| 4 |
Account Compression Authority |
Signs CPI calls from the Light System Program to the Account Compression Program. |
| 5 |
Account Compression Program |
* Writes to state and address tree accounts.
* Clients and the Account Compression Program do not interact directly — handled internally.
|
| 6 |
System Program |
Solana 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 theme={null}
// 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();
```
```typescript theme={null}
// 1. Initialize helper
const packedAccounts
= new PackedAccounts();
// 2. Add system accounts
const systemAccountConfig
= SystemAccountMetaConfig.new(programId);
packedAccounts.addSystemAccounts(systemAccountConfig);
// 3. Get indices for tree accounts
const merkleTreePubkeyIndex
= packedAccounts.insertOrGet(compressedAccount.treeInfo.tree);
const queuePubkeyIndex
= packedAccounts.insertOrGet(compressedAccount.treeInfo.queue);
const packedInputAccounts = {
merkleTreePubkeyIndex,
queuePubkeyIndex,
leafIndex: proofRpcResult.leafIndices[0],
rootIndex: proofRpcResult.rootIndices[0],
};
const outputStateTreeIndex
= packedAccounts.insertOrGet(outputStateTree);
// 4. Convert to Account Metas
const { remainingAccounts }
= packedAccounts.toAccountMetas();
```
```rust theme={null}
// 1. Initialize helper
let mut remaining_accounts = PackedAccounts::default();
// 2. Add system accounts
let config
= SystemAccountMetaConfig::new(program_id);
remaining_accounts.add_system_accounts(config)?;
// 3. Get indices for tree accounts
let packed_accounts
= rpc_result.pack_tree_infos(&mut remaining_accounts);
// 4. Get index for output state tree
let output_state_tree_info = rpc.get_random_state_tree_info()?;
let output_state_tree_index
= output_state_tree_info.pack_output_tree_index(&mut remaining_accounts)?;
// 5. Convert to Account Metas
let (remaining_accounts_metas, _, _)
= remaining_accounts.to_account_metas();
```
```rust theme={null}
// 1. Initialize helper
let mut remaining_accounts = PackedAccounts::default();
// 2. Add system accounts
let config
= SystemAccountMetaConfig::new(program_id);
remaining_accounts.add_system_accounts(config)?;
// 3. Get indices for tree accounts
let packed_tree_accounts = rpc_result
.pack_tree_infos(&mut remaining_accounts)
.state_trees // includes output_state_tree_index
.unwrap();
// 4. Convert to Account Metas
let (remaining_accounts_metas, _, _)
= remaining_accounts.to_account_metas();
```
Depending on your instruction you must include different tree and queue accounts.
| Instruction |
Address Tree |
State Tree (includes nullifier queue) |
Output Queue |
| Create |
✓ |
- |
✓ |
| Update / Close / Reinit |
- |
✓ |
✓ |
| Burn |
- |
✓ |
- |
* **Address tree**: only used to derive and store a new address.
* **State tree**: used to reference the existing compressed account hash. Therefore not used by create. The state tree and nullifier queue are combined into a single account.
* **Output Queue**: used to store compressed account hashes. A forester node updates the state tree asynchronously.
* **Create only** - Choose any available queue, or use a pre-selected queue to store the new compressed account.
* **Update/Close/Reinit** - Use the queue of the existing compressed account as output queue.
* **Mixed instructions (create + update in same tx)** - Use the queue from the existing account as output queue.
* **Burn** - Do not include an output queue.
## Instruction Data
Build your instruction data with the validity proof, tree account indices, and account data.
```typescript theme={null}
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**
```typescript theme={null}
const proof = {
0: proofRpcResult.compressedProof,
};
const instructionData = {
proof,
accountMeta: {
treeInfo: packedStateTreeInfo,
address: compressedAccount.address,
outputStateTreeIndex: outputStateTreeIndex
},
currentMessage: currentAccount.message,
newMessage,
};
```
1. Include `proof` to to prove the **account hash exists** in the state tree
2. Specify the existing accounts address, its `packedStateTreeInfo` and the output state tree to store the updated compressed account hash.
3. Pass **current account data** and **new data**
Use the state tree of the existing compressed account as output state tree.
```typescript theme={null}
const proof = {
0: proofRpcResult.compressedProof,
};
const instructionData = {
proof,
accountMeta: {
treeInfo: packedStateTreeInfo,
address: compressedAccount.address,
outputStateTreeIndex: outputStateTreeIndex
},
currentMessage: currentAccount.message,
};
```
1. Include `proof` to prove the **account hash exists** in the state tree
2. Specify the existing accounts address, its `packedStateTreeInfo` and the output state tree to store the **hash with zero values** for the closed account.
3. Pass **current account data**
Use the state tree of the existing compressed account as output state tree.
```typescript theme={null}
const proof = {
0: proofRpcResult.compressedProof,
};
const instructionData = {
proof,
accountMeta: {
treeInfo: packedStateTreeInfo,
address: compressedAccount.address,
outputStateTreeIndex: outputStateTreeIndex
},
};
```
1. Include `proof` to prove the **account hash exists** in the state tree
2. Specify the existing accounts address, its `packedStateTreeInfo` and the output state tree that will store the reinitialized account hash
3. Reinitialize creates an account with **default-initialized values**
* These values are `Pubkey` as all zeros, numbers as `0`, strings as empty.
* To set custom values, update the account in the same or a separate transaction.
Use the state tree of the existing compressed account as output state tree.
```typescript theme={null}
const proof = {
0: proofRpcResult.compressedProof,
};
const instructionData = {
proof,
accountMeta: {
treeInfo: packedStateTreeInfo,
address: compressedAccount.address
},
currentMessage: currentAccount.message,
};
```
1. Include `proof` to prove the **account hash exists** in the state tree
2. Specify the existing accounts address and its `packedStateTreeInfo`. You don't need to specify the output state tree, since burn permanently removes the account.
3. Pass **current account data**
```rust theme={null}
let instruction_data = create::instruction::CreateAccount {
proof: rpc_result.proof,
address_tree_info: packed_accounts.address_trees[0],
output_state_tree_index: output_state_tree_index,
message,
}
.data();
```
1. Include `proof` to prove the **address does not exist** in the address tree
2. Specify **address tree and output state tree** to where you packed accounts
3. Pass **initial account data**
```rust theme={null}
let instruction_data = update::instruction::UpdateAccount {
proof: rpc_result.proof,
current_account,
account_meta: CompressedAccountMeta {
tree_info: packed_tree_accounts.packed_tree_infos[0],
address: compressed_account.address.unwrap(),
output_state_tree_index: packed_tree_accounts.output_tree_index,
},
new_message,
}
.data();
```
Use the state tree of the existing compressed account as output state tree.
1. Include `proof` to prove the **account hash exists** in the state tree
2. Specify the existing accounts address, its `packed_tree_infos` and the output state tree to store the updated compressed account hash
3. Pass **current account data** and **new data**
```rust theme={null}
let instruction_data = close::instruction::CloseAccount {
proof: rpc_result.proof,
account_meta: CompressedAccountMeta {
tree_info: packed_tree_accounts.packed_tree_infos[0],
address: compressed_account.address.unwrap(),
output_state_tree_index: packed_tree_accounts.output_tree_index,
},
current_message,
}
.data();
```
Use the state tree of the existing compressed account as output state tree.
1. Include `proof` to prove the **account hash exists** in the state tree
2. Specify the existing accounts address, its `packed_tree_infos` and the output state tree to store the **hash with zero values** for the closed account
3. Pass **current account data**
```rust theme={null}
let instruction_data = reinit::instruction::ReinitAccount {
proof: rpc_result.proof,
account_meta: CompressedAccountMeta {
tree_info: packed_tree_accounts.packed_tree_infos[0],
address: compressed_account.address.unwrap(),
output_state_tree_index: packed_tree_accounts.output_tree_index,
},
}
.data();
```
Use the state tree of the existing compressed account as output state tree.
1. Include `proof` to prove the **account hash exists** in the state tree
2. Specify the existing accounts address, its `packed_tree_infos` and the output state tree that will store the reinitialized account hash
3. Reinitialize creates an account with **default-initialized values**
* These values are `Pubkey` as all zeros, numbers as `0`, strings as empty.
* To set custom values, update the account in the same or a separate transaction.
```rust theme={null}
let instruction_data = burn::instruction::BurnAccount {
proof: rpc_result.proof,
account_meta: CompressedAccountMetaBurn {
tree_info: packed_tree_accounts.packed_tree_infos[0],
address: compressed_account.address.unwrap(),
},
current_message,
}
.data();
```
1. Include `proof` to prove the **account hash exists** in the state tree
2. Specify the existing accounts address and its `packed_tree_infos`. You don't need to specify the output state tree, since burn permanently removes the account
3. Pass **current 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.
## 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 theme={null}
// Accounts
// ┌-------------------------------┐
// .accounts() .remainingAccounts()
// [custom] [PackedAccounts]
const instruction = await program.methods
.yourInstruction(instructionData)
.accounts({
signer: signer.publicKey,
})
.remainingAccounts(remainingAccounts)
.instruction();
```
```rust theme={null}
// Accounts
// ┌---------------------------------┐
// [custom accounts] [PackedAccounts]
let accounts = [vec![AccountMeta::new(payer.pubkey(), true)], remaining_accounts].concat();
let instruction = Instruction {
program_id: program_id,
accounts,
data: instruction_data,
};
```
## Send Transaction
```text theme={null}
---
description: Write client code to interact with compressed PDA programs
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Write client code to interact with compressed PDA programs
Context:
- Guide: https://zkcompression.com/compressed-pdas/guides/client-guide
- Skills and resources index: https://zkcompression.com/skill.md
- TS packages: @lightprotocol/stateless.js, @lightprotocol/compressed-token, @solana/web3.js
- Rust crates: light-client, light-sdk
- TS example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/create/tests
- Rust example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/create/programs/create/tests
- All operations: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor
Key APIs:
- TS: createRpc(), deriveAddressSeedV2(), deriveAddressV2(), getValidityProofV0(), PackedAccounts, SystemAccountMetaConfig, getStateTreeInfos(), selectStateTreeInfo()
- Rust: LightClientConfig, LightClient, derive_address(), get_validity_proof(), PackedAccounts, SystemAccountMetaConfig, get_random_state_tree_info()
### 1. Index project
- Grep `@lightprotocol|stateless\.js|createRpc|deriveAddress|getValidityProof|PackedAccounts|light_client|light_sdk|derive_address|get_validity_proof` across src/
- Glob `**/*.ts` and `**/*.rs` for project structure
- Identify: RPC setup, existing compressed account operations, program ID references
- Check package.json or Cargo.toml for existing light-* dependencies
- Task subagent (Grep/Read/WebFetch) if project has multiple packages to scan in parallel
### 2. Read references
- WebFetch the guide above — review both TypeScript and Rust code samples
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: TypeScript or Rust?
- AskUserQuestion: what is the goal? (new client from scratch, add client calls to existing project, migrate from regular accounts)
- AskUserQuestion: which operations? (create only, full CRUD, specific subset like create + update)
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan: which files to modify, what code to add, dependency changes
- Follow the guide's step order: Setup → Address derivation → Validity proof → PackedAccounts → Instruction data → Build instruction → Send transaction
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- For TypeScript: Bash `npm install @lightprotocol/stateless.js @lightprotocol/compressed-token @solana/web3.js`
- For Rust: Bash `cargo add light-client@0.23 light-sdk@0.23`
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- TypeScript: Bash `tsc --noEmit` + run existing test suite if present
- Rust: Bash `cargo check` + `cargo test` if tests exist
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work
```
# Full Code Examples
Find the source code [here](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/create/tests).
```typescript theme={null}
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,
deriveAddressV2,
deriveAddressSeedV2,
batchAddressTree,
PackedAccounts,
Rpc,
sleep,
SystemAccountMetaConfig,
selectStateTreeInfo,
TreeInfo,
} 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;
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 stateTreeInfos = await rpc.getStateTreeInfos();
const stateTreeInfo = selectStateTreeInfo(stateTreeInfos);
const addressTree = new web3.PublicKey(batchAddressTree);
const messageSeed = new TextEncoder().encode("message");
const seed = deriveAddressSeedV2([messageSeed, signer.publicKey.toBytes()]);
const address = deriveAddressV2(
seed,
addressTree,
new web3.PublicKey(program.idl.address),
);
// Create compressed account with message
const txId = await createCompressedAccount(
rpc,
addressTree,
address,
program,
stateTreeInfo,
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,
address: anchor.web3.PublicKey,
program: anchor.Program,
stateTreeInfo: TreeInfo,
signer: anchor.web3.Keypair,
message: string,
) {
const proofRpcResult = await rpc.getValidityProofV0(
[],
[
{
tree: addressTree,
queue: addressTree,
address: bn(address.toBytes()),
},
],
);
const systemAccountConfig = new SystemAccountMetaConfig(program.programId);
let remainingAccounts = new PackedAccounts();
remainingAccounts.addSystemAccountsV2(systemAccountConfig);
const addressMerkleTreePubkeyIndex =
remainingAccounts.insertOrGet(addressTree);
const addressQueuePubkeyIndex = addressMerkleTreePubkeyIndex;
const packedAddressTreeInfo = {
rootIndex: proofRpcResult.rootIndices[0],
addressMerkleTreePubkeyIndex,
addressQueuePubkeyIndex,
};
const outputStateTreeIndex =
remainingAccounts.insertOrGet(stateTreeInfo.queue);
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 the source code [here](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/create/programs/create/tests).
```rust theme={null}
#![cfg(feature = "test-sbf")]
use anchor_lang::AnchorDeserialize;
use light_program_test::{
program_test::LightProgramTest, AddressWithTree, Indexer, ProgramTestConfig, Rpc, RpcError,
};
use light_sdk::{
address::v2::derive_address,
instruction::{PackedAccounts, SystemAccountMetaConfig},
};
use create::MyCompressedAccount;
use solana_sdk::{
instruction::{AccountMeta, Instruction},
signature::{Keypair, Signature, Signer},
};
#[tokio::test]
async fn test_create() {
let config = ProgramTestConfig::new(true, Some(vec![("create", create::ID)]));
let mut rpc = LightProgramTest::new(config).await.unwrap();
let payer = rpc.get_payer().insecure_clone();
let address_tree_info = rpc.get_address_tree_v2();
let (address, _) = derive_address(
&[b"message", payer.pubkey().as_ref()],
&address_tree_info.tree,
&create::ID,
);
create_compressed_account(&mut rpc, &payer, &address, "Hello, compressed world!".to_string())
.await
.unwrap();
let compressed_account = rpc
.get_compressed_account(address, None)
.await
.unwrap()
.value
.unwrap();
let data = &compressed_account.data.as_ref().unwrap().data;
let account = MyCompressedAccount::deserialize(&mut &data[..]).unwrap();
assert_eq!(account.owner, payer.pubkey());
assert_eq!(account.message, "Hello, compressed world!");
}
async fn create_compressed_account(
rpc: &mut LightProgramTest,
payer: &Keypair,
address: &[u8; 32],
message: String,
) -> Result {
let config = SystemAccountMetaConfig::new(create::ID);
let mut remaining_accounts = PackedAccounts::default();
remaining_accounts.add_system_accounts_v2(config)?;
let address_tree_info = rpc.get_address_tree_v2();
let rpc_result = rpc
.get_validity_proof(
vec![],
vec![AddressWithTree {
address: *address,
tree: address_tree_info.tree,
}],
None,
)
.await?
.value;
let packed_accounts = rpc_result.pack_tree_infos(&mut remaining_accounts);
let output_state_tree_index = rpc
.get_random_state_tree_info()?
.pack_output_tree_index(&mut remaining_accounts)?;
let (remaining_accounts, _, _) = remaining_accounts.to_account_metas();
let instruction = Instruction {
program_id: create::ID,
accounts: [
vec![AccountMeta::new(payer.pubkey(), true)],
remaining_accounts,
]
.concat(),
data: {
use anchor_lang::InstructionData;
create::instruction::CreateAccount {
proof: rpc_result.proof,
address_tree_info: packed_accounts.address_trees[0],
output_state_tree_index: output_state_tree_index,
message,
}
.data()
},
};
rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer])
.await
}
```
Find all [full code examples with Rust and TypeScript tests here](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor) 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](/resources/error-cheatsheet) and [AskDevin](https://deepwiki.com/Lightprotocol/light-protocol/3.1-javascripttypescript-sdks).
# Next Steps
# Burn Compressed Accounts
Source: https://www.zkcompression.com/compressed-pdas/guides/how-to-burn-compressed-accounts
Guide to burn compressed accounts in Solana programs with full code examples.
Compressed accounts are permanently burned via CPI to the Light System Program.
Burning a compressed account
* consumes the existing account hash, and
* produces no output state.
* A burned account cannot be reinitialized.
Find [full code examples at the end](/compressed-pdas/guides/how-to-burn-compressed-accounts#full-code-example) for Anchor and native Rust.
# Implementation Guide
This guide will cover the components of a Solana program that burns compressed accounts.\
Here is the complete flow to burn compressed accounts:
### Program Setup
**Dependencies**
Add dependencies to your program.
```toml Anchor theme={null}
[dependencies]
light-sdk = "0.23.0"
anchor_lang = "0.31.1"
```
```toml Native Rust theme={null}
[dependencies]
light-sdk = "0.23.0"
borsh = "0.10.4"
solana-program = "2.2"
```
* The `light-sdk` provides macros, wrappers and CPI interface to create and interact with compressed accounts.
* Add the serialization library (`borsh` for native Rust, or use `AnchorSerialize`).
**Constants**
Set program address and derive the CPI authority PDA to call the Light System program.
```rust theme={null}
declare_id!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");
pub const LIGHT_CPI_SIGNER: CpiSigner =
derive_light_cpi_signer!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");
```
**`CPISigner`** is the configuration struct for CPI's to the Light System Program.
* CPIs to the Light System program must be signed with a PDA derived by your program with the seed `b"authority"`
* `derive_light_cpi_signer!` derives the CPI signer PDA for you at compile time.
**Compressed Account**
Define your compressed account struct.
```rust Anchor theme={null}
#[event] // declared as event so that it is part of the idl.
#[derive(
Clone,
Debug,
Default,
LightDiscriminator
)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
```
```rust Native Rust theme={null}
#[derive(
Debug,
Default,
Clone,
BorshSerialize,
BorshDeserialize,
LightDiscriminator,
)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
```
You derive
* the standard traits (`Clone`, `Debug`, `Default`),
* `borsh` or `AnchorSerialize` to serialize account data, and
* `LightDiscriminator` to implements a unique type ID (8 bytes) to distinguish account types. The default compressed account layout enforces a discriminator in its *own field*, not the first 8 bytes of the data field.
The traits listed above are required for `LightAccount`. `LightAccount` wraps `MyCompressedAccount` in Step 3 to set the discriminator and create the compressed account's data.
### Instruction Data
Define the instruction data with the following parameters:
```rust theme={null}
pub fn burn_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
account_meta: CompressedAccountMetaBurn,
current_message: String,
) -> Result<()>
```
```rust theme={null}
pub struct BurnInstructionData {
pub proof: ValidityProof,
pub account_meta: CompressedAccountMetaBurn,
pub current_account: MyCompressedAccount,
}
```
1. **Validity Proof**
* Define `proof` to include the proof that the account exists in the state tree.
* Clients fetch a validity proof with `getValidityProof()` from an RPC provider that supports ZK Compression (Helius, Triton, ...).
2. **Specify input state**
* Define `account_meta: CompressedAccountMetaBurn` to reference the existing account for the Light System Program to nullify permanently:
* `tree_info: PackedStateTreeInfo`: References the existing account hash in the state tree.
* `address`: The account's derived address.
Burn does not specify an output state tree. `CompressedAccountMetaBurn` omits `output_state_tree_index` because no output state is created.
3. **Current account data**
* Define fields to include the current account data passed by the client.
* This depends on your program logic. This example includes `current_message` (or `current_account` in Native Rust).
### Burn Compressed Account
Burn the compressed account permanently with `LightAccount::new_burn()`. No account can be reinitialized at this address in the future.
`new_burn()`
1. hashes the current account data as input state and
2. creates no output state to burn the account permanently.
```rust theme={null}
let my_compressed_account = LightAccount::::new_burn(
&crate::ID,
&account_meta,
MyCompressedAccount {
owner: ctx.accounts.signer.key(),
message: current_message,
},
)?;
```
```rust theme={null}
let my_compressed_account = LightAccount::::new_burn(
&ID,
&instruction_data.account_meta,
instruction_data.current_account,
)?;
```
**Pass these parameters to `new_burn()`:**
* `&program_id`: The program's ID that owns the compressed account.
* `&account_meta`: The `CompressedAccountMetaBurn` from instruction data (*Step 2*) that identifies the existing account for the Light System Program to nullify permanently.
* Anchor: Pass `&account_meta` directly
* Native Rust: Pass `&instruction_data.account_meta`
* Include the curent account data.
* Anchor: Build `MyCompressedAccount` with `owner` and `message`.
* Native Rust: Pass `instruction_data.current_account` directly.
**The SDK creates:**
* A `LightAccount` wrapper that marks the account as permanently burned with no output state.
`new_burn()` hashes the input state. The Light System Program verifies the input hash and nullifies it in *Step 4*.
### Light System Program CPI
The Light System Program CPI burns the compressed account permanently.
The Light System Program
* validates the account exists in state tree with the validity,
* nullifies the existing account hash, and
* creates no output state.
```rust theme={null}
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(my_compressed_account)?
.invoke(light_cpi_accounts)?;
```
**Set up `CpiAccounts::new()`:**
`CpiAccounts::new()` parses accounts for the CPI call to Light System Program.
**Pass these parameters:**
* `ctx.accounts.signer.as_ref()`: the transaction signer
* `ctx.remaining_accounts`: Slice with `[system_accounts, ...packed_tree_accounts]`. The client builds this with `PackedAccounts` and passes it to the instruction.
* `&LIGHT_CPI_SIGNER`: Your program's CPI signer PDA defined in Constants.
```rust theme={null}
let (signer, remaining_accounts) = accounts
.split_first();
let cpi_accounts = CpiAccounts::new(
signer,
remaining_accounts,
LIGHT_CPI_SIGNER
);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.invoke(cpi_accounts)?;
```
**Set up `CpiAccounts::new()`:**
`CpiAccounts::new()` parses accounts for the CPI call to Light System Program.
**Pass these parameters:**
* `signer`: account that signs and pays for the transaction
* `remaining_accounts`: Slice with `[system_accounts, ...packed_tree_accounts]`. The client builds this with `PackedAccounts`.
* `split_first()` extracts the fee payer from the accounts array to separate it from the Light System Program accounts needed for the CPI.
* `&LIGHT_CPI_SIGNER`: Your program's CPI signer PDA defined in Constants.
|
|
|
| 1 |
Light System Program |
Verifies validity proofs, compressed account ownership checks, and CPIs the Account Compression Program to update tree accounts. |
| 2 |
CPI 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.
|
| 3 |
Registered Program PDA |
Provides access control to the Account Compression Program. |
| 4 |
Account Compression Authority |
Signs CPI calls from the Light System Program to the Account Compression Program. |
| 5 |
Account Compression Program |
* Writes to state and address tree accounts.
* Clients and the Account Compression Program do not interact directly — handled internally.
|
| 6 |
System Program |
Solana System Program used to transfer lamports. |
**Build the CPI instruction**:
* `new_cpi()` initializes the CPI instruction with the `proof` to prove the account exists in the state tree *- defined in the Instruction Data (Step 2).*
* `with_light_account` adds the `LightAccount` wrapper configured to burn the account *- defined in Step 3*.
* `invoke(light_cpi_accounts)` calls the Light System Program with `CpiAccounts`.
```text theme={null}
---
description: Build a program that permanently burns compressed accounts
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Build a program that permanently burns compressed accounts
Context:
- Guide: https://zkcompression.com/compressed-pdas/guides/how-to-burn-compressed-accounts
- Skills and resources index: https://zkcompression.com/skill.md
- Crate: light-sdk (LightAccount, CpiAccounts, LightSystemProgramCpi, derive_light_cpi_signer!)
- Anchor example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/burn
- Native Rust example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/burn
Key SDK API: LightAccount::new_burn()
### 1. Index project
- Grep `declare_id|#\[program\]|entrypoint!|Pubkey|AccountInfo|burn|destroy|permanent` across src/
- Glob `**/*.rs` and `**/Cargo.toml` for project structure
- Identify: program ID, existing instructions, account structs, framework (Anchor or native)
- Read Cargo.toml — note existing dependencies and Solana SDK version
- Task subagent (Grep/Read/WebFetch) if project has multiple crates to scan in parallel
### 2. Read references
- WebFetch the guide above — review both Anchor and Native Rust code samples
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: what is the goal? (new program from scratch, add account burn to existing program, migrate from regular accounts)
- AskUserQuestion: Anchor or Native Rust framework?
- AskUserQuestion: does the program already have compressed account instructions, or is this the first one?
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan: which files to modify, what code to add, dependency changes
- Follow the guide's step order: Dependencies → Constants → Account Struct → Instruction Data → Burn Compressed Account → Light System Program CPI
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- Add deps: Bash `cargo add light-sdk@0.23` (add `anchor_lang@0.31` for Anchor or `solana-program@2.2` + `borsh@0.10` for Native Rust)
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- Bash `cargo build-sbf` or `anchor build`
- Bash `cargo test-sbf` or `anchor test` if tests exist
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work
```
# Full Code Example
The example programs below implement all steps from this guide.
**Install Solana CLI:**
```bash theme={null}
sh -c "$(curl -sSfL https://release.solana.com/v2.2.15/install)"
```
**Install Anchor CLI:**
```bash theme={null}
cargo install --git https://github.com/coral-xyz/anchor avm --force
avm install latest
avm use latest
```
**Install the Light CLI:**
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
**Verify installation:**
```bash theme={null}
light --version
```
For help with debugging, see the [Error Cheatsheet](/resources/error-cheatsheet).
Find the source code [here](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/burn).
````rust theme={null}
#![allow(unexpected_cfgs)]
#![allow(deprecated)]
use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize};
use light_sdk::{
account::LightAccount,
address::v2::derive_address,
cpi::{v2::CpiAccounts, CpiSigner},
derive_light_cpi_signer,
instruction::{account_meta::CompressedAccountMetaBurn, PackedAddressTreeInfo, ValidityProof},
LightDiscriminator, PackedAddressTreeInfoExt,
};
use light_sdk::constants::ADDRESS_TREE_V2;
declare_id!("BJhPWQnD31mdo6739Mac1gLuSsbbwTmpgjHsW6shf6WA");
pub const LIGHT_CPI_SIGNER: CpiSigner =
derive_light_cpi_signer!("BJhPWQnD31mdo6739Mac1gLuSsbbwTmpgjHsW6shf6WA");
#[program]
pub mod burn {
use super::*;
use light_sdk::cpi::{
v2::LightSystemProgramCpi, InvokeLightSystemProgram, LightCpiInstruction,
};
/// Setup: Creates a compressed account
pub fn create_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
address_tree_info: PackedAddressTreeInfo,
output_state_tree_index: u8,
message: String,
) -> Result<()> {
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
let address_tree_pubkey = address_tree_info
.get_tree_pubkey(&light_cpi_accounts)
.map_err(|_| ErrorCode::AccountNotEnoughKeys)?;
if address_tree_pubkey.to_bytes() != ADDRESS_TREE_V2 {
msg!("Invalid address tree");
return Err(ProgramError::InvalidAccountData.into());
}
let (address, address_seed) = derive_address(
&[b"message", ctx.accounts.signer.key().as_ref()],
&address_tree_pubkey,
&crate::ID,
);
let mut my_compressed_account = LightAccount::::new_init(
&crate::ID,
Some(address),
output_state_tree_index,
);
my_compressed_account.owner = ctx.accounts.signer.key();
my_compressed_account.message = message.clone();
msg!(
"Created compressed account with message: {}",
my_compressed_account.message
);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(my_compressed_account)?
.with_new_addresses(&[address_tree_info.into_new_address_params_assigned_packed(address_seed, Some(0))])
.invoke(light_cpi_accounts)?;
Ok(())
}
/// Burns a compressed account permanently
pub fn burn_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
account_meta: CompressedAccountMetaBurn,
current_message: String,
) -> Result<()> {
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
let my_compressed_account = LightAccount::::new_burn(
&crate::ID,
&account_meta,
MyCompressedAccount {
owner: ctx.accounts.signer.key(),
message: current_message,
},
)?;
msg!("Burning compressed account permanently");
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(my_compressed_account)?
.invoke(light_cpi_accounts)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct GenericAnchorAccounts<'info> {
#[account(mut)]
pub signer: Signer<'info>,
}
#[event]
#[derive(Clone, Debug, Default, LightDiscriminator)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}```
````
Find the source code [here](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/burn).
```rust theme={null}
#![allow(unexpected_cfgs)]
#[cfg(any(test, feature = "test-helpers"))]
pub mod test_helpers;
use borsh::{BorshDeserialize, BorshSerialize};
use light_macros::pubkey;
use light_sdk::{
account::sha::LightAccount,
address::v2::derive_address,
cpi::{
v2::{CpiAccounts, LightSystemProgramCpi},
CpiSigner, InvokeLightSystemProgram, LightCpiInstruction,
},
derive_light_cpi_signer,
error::LightSdkError,
instruction::{account_meta::CompressedAccountMetaBurn, PackedAddressTreeInfo, ValidityProof},
LightDiscriminator,
};
use light_sdk::constants::ADDRESS_TREE_V2;
use light_sdk::PackedAddressTreeInfoExt;
use solana_program::{
account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey,
};
pub const ID: Pubkey = pubkey!("CFWrQ8za2yT1xH8yBjYvsDUCWnBH7vXtyVJwqoX5FcNg");
pub const LIGHT_CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("CFWrQ8za2yT1xH8yBjYvsDUCWnBH7vXtyVJwqoX5FcNg");
#[cfg(not(feature = "no-entrypoint"))]
entrypoint!(process_instruction);
#[derive(Debug, BorshSerialize, BorshDeserialize)]
pub enum InstructionType {
Create,
Burn,
}
#[derive(Debug, BorshSerialize, BorshDeserialize)]
pub struct CreateInstructionData {
pub proof: ValidityProof,
pub address_tree_info: PackedAddressTreeInfo,
pub output_state_tree_index: u8,
pub message: String,
}
#[derive(Debug, BorshSerialize, BorshDeserialize)]
pub struct BurnInstructionData {
pub proof: ValidityProof,
pub account_meta: CompressedAccountMetaBurn,
pub current_account: MyCompressedAccount,
}
#[derive(Debug, Default, Clone, BorshSerialize, BorshDeserialize, LightDiscriminator)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> Result<(), ProgramError> {
let (instruction_type, rest) = instruction_data
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
match InstructionType::try_from_slice(&[*instruction_type])
.map_err(|_| ProgramError::InvalidInstructionData)?
{
InstructionType::Create => create(accounts, rest)?,
InstructionType::Burn => burn(accounts, rest)?,
}
Ok(())
}
fn create(accounts: &[AccountInfo], instruction_data: &[u8]) -> Result<(), LightSdkError> {
let instruction_data =
CreateInstructionData::try_from_slice(instruction_data).map_err(|_| LightSdkError::Borsh)?;
let signer = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?;
let light_cpi_accounts = CpiAccounts::new(signer, &accounts[1..], LIGHT_CPI_SIGNER);
let address_tree_pubkey = instruction_data
.address_tree_info
.get_tree_pubkey(&light_cpi_accounts)
.map_err(|_| ProgramError::NotEnoughAccountKeys)?;
if address_tree_pubkey.to_bytes() != ADDRESS_TREE_V2 {
solana_program::msg!("Invalid address tree");
return Err(LightSdkError::ProgramError(ProgramError::InvalidAccountData));
}
let (address, address_seed) = derive_address(
&[b"message", signer.key.as_ref()],
&address_tree_pubkey,
&ID,
);
let new_address_params = instruction_data
.address_tree_info
.into_new_address_params_assigned_packed(address_seed, Some(0));
let mut my_compressed_account = LightAccount::::new_init(
&ID,
Some(address),
instruction_data.output_state_tree_index,
);
my_compressed_account.owner = *signer.key;
my_compressed_account.message = instruction_data.message;
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.with_new_addresses(&[new_address_params])
.invoke(light_cpi_accounts)?;
Ok(())
}
fn burn(accounts: &[AccountInfo], instruction_data: &[u8]) -> Result<(), LightSdkError> {
let instruction_data =
BurnInstructionData::try_from_slice(instruction_data).map_err(|_| LightSdkError::Borsh)?;
let (signer, remaining_accounts) = accounts
.split_first()
.ok_or(ProgramError::InvalidAccountData)?;
let cpi_accounts = CpiAccounts::new(signer, remaining_accounts, LIGHT_CPI_SIGNER);
let my_compressed_account = LightAccount::::new_burn(
&ID, // Now the burn program owns the account since it created it
&instruction_data.account_meta,
instruction_data.current_account,
)?;
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.invoke(cpi_accounts)?;
Ok(())
}
```
# Next Steps
# Close Compressed Accounts
Source: https://www.zkcompression.com/compressed-pdas/guides/how-to-close-compressed-accounts
Guide to close compressed accounts in Solana programs with full code examples.
Compressed accounts are closed via CPI to the Light System Program.
Closing a compressed account
* consumes the existing account hash, and
* produces a new account hash with zero values to mark it as closed.
* A closed compressed account [can be reinitialized](/compressed-pdas/guides/how-to-reinitialize-compressed-accounts).
Find [full code examples at the end](/compressed-pdas/guides/how-to-close-compressed-accounts#full-code-example) for Anchor and native Rust.
# Implementation Guide
This guide will cover the components of a Solana program that closes compressed accounts.
Here is the complete flow to close compressed accounts:
### Program Setup
**Dependencies**
Add dependencies to your program.
```toml Anchor theme={null}
[dependencies]
light-sdk = "0.23.0"
anchor_lang = "0.31.1"
```
```toml Native Rust theme={null}
[dependencies]
light-sdk = "0.23.0"
borsh = "0.10.4"
solana-program = "2.2"
```
* The `light-sdk` provides macros, wrappers and CPI interface to create and interact with compressed accounts.
* Add the serialization library (`borsh` for native Rust, or use `AnchorSerialize`).
**Constants**
Set program address and derive the CPI authority PDA to call the Light System program.
```rust theme={null}
declare_id!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");
pub const LIGHT_CPI_SIGNER: CpiSigner =
derive_light_cpi_signer!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");
```
**`CPISigner`** is the configuration struct for CPI's to the Light System Program.
* CPIs to the Light System program must be signed with a PDA derived by your program with the seed `b"authority"`
* `derive_light_cpi_signer!` derives the CPI signer PDA for you at compile time.
**Compressed Account**
Define your compressed account struct.
```rust Anchor theme={null}
#[event] // declared as event so that it is part of the idl.
#[derive(
Clone,
Debug,
Default,
LightDiscriminator
)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
```
```rust Native Rust theme={null}
#[derive(
Debug,
Default,
Clone,
BorshSerialize,
BorshDeserialize,
LightDiscriminator,
)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
```
You derive
* the standard traits (`Clone`, `Debug`, `Default`),
* `borsh` or `AnchorSerialize` to serialize account data, and
* `LightDiscriminator` to implements a unique type ID (8 bytes) to distinguish account types. The default compressed account layout enforces a discriminator in its *own field*, not the first 8 bytes of the data field.
The traits listed above are required for `LightAccount`. `LightAccount` wraps `MyCompressedAccount` in Step 3 to set the discriminator and create the compressed account's data.
### Instruction Data
Define the instruction data with the following parameters:
```rust theme={null}
pub fn close_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
account_meta: CompressedAccountMeta,
current_message: String,
) -> Result<()>
```
```rust theme={null}
pub struct CloseInstructionData {
pub proof: ValidityProof,
pub account_meta: CompressedAccountMeta,
pub current_message: String,
}
```
1. **Validity Proof**
* Define `proof` to include the proof that the account exists in the state tree.
* Clients fetch a validity proof with `getValidityProof()` from an RPC provider that supports ZK Compression (Helius, Triton, ...).
2. **Specify input state and output state tree (stores closed account hash)**
* Define `account_meta: CompressedAccountMeta` to reference the existing account and specify the state tree to store the new hash with zero values:
* `tree_info: PackedStateTreeInfo`: References the existing account hash in the state tree.
* `address`: The account's derived address.
* `output_state_tree_index` points to the state tree that will store the updated hash with a zero-byte hash to mark the account as closed.
Clients fetch the current account with `getCompressedAccount()` and populate `CompressedAccountMeta` with the account's metadata.
3. **Current data**
* Define fields to include the current account data passed by the client.
* This depends on your program logic. This example includes the `current_message` field.
### Close Compressed Account
Load the compressed account and mark it as closed with `LightAccount::new_close()`.
`new_close()`
1. hashes the current account data as input state and
2. marks the account for closure for the Light System Program.
```rust theme={null}
let my_compressed_account = LightAccount::::new_close(
&crate::ID,
&account_meta,
MyCompressedAccount {
owner: ctx.accounts.signer.key(),
message: current_message,
},
)?;
```
```rust theme={null}
let my_compressed_account = LightAccount::::new_close(
&ID,
&instruction_data.account_meta,
MyCompressedAccount {
owner: *signer.key,
message: instruction_data.current_message,
},
)?;
```
**Pass these parameters to `new_close()`:**
* `&program_id`: The program's ID that owns the compressed account.
* `&account_meta`: The `CompressedAccountMeta` from instruction data (*Step 2*) that identifies the existing account and specifies the output state tree.
* Current account data: The existing account data. The SDK hashes this input state for verification by the Light System Program.
* Anchor: Construct `MyCompressedAccount` with `ctx.accounts.signer.key()` and `current_message`
* Native: Construct `MyCompressedAccount` with data from `instruction_data`
**The SDK creates:**
* A `LightAccount` wrapper similar to Anchor's `Account` that marks the account for closure.
`new_close()` hashes the input state and marks the account for closure. The Light System Program creates output state with zero values:
* a zero discriminator (`0u8; 8`) removes type identification of the account,
* the output contains zeroes as data hash that indicates no data content, and
* the data field contains an empty vector, instead of serialized account fields.
### Light System Program CPI
Invoke the Light System Program to close the compressed account. This empty account can be reinitialized with `LightAccount::new_empty()`.
The Light System Program
* validates the account exists in state tree,
* nullifies the existing account hash, and
* appends the new account hash with zero values to the state tree to mark it as closed.
```rust theme={null}
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(my_compressed_account)?
.invoke(light_cpi_accounts)?;
```
**Set up `CpiAccounts::new()`:**
`CpiAccounts::new()` parses accounts for the CPI call to Light System Program.
**Pass these parameters:**
* `ctx.accounts.signer.as_ref()`: the transaction signer
* `ctx.remaining_accounts`: Slice with `[system_accounts, ...packed_tree_accounts]`. The client builds this with `PackedAccounts` and passes it to the instruction.
* `&LIGHT_CPI_SIGNER`: Your program's CPI signer PDA defined in Constants.
```rust theme={null}
let (signer, remaining_accounts) = accounts
.split_first();
let cpi_accounts = CpiAccounts::new(
signer,
remaining_accounts,
LIGHT_CPI_SIGNER
);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.invoke(cpi_accounts)?;
```
**Set up `CpiAccounts::new()`:**
`CpiAccounts::new()` parses accounts for the CPI call to Light System Program.
**Pass these parameters:**
* `signer`: account that signs and pays for the transaction
* `remaining_accounts`: Slice with `[system_accounts, ...packed_tree_accounts]`. The client builds this with `PackedAccounts`.
* `split_first()` extracts the fee payer from the accounts array to separate it from the Light System Program accounts needed for the CPI.
* `&LIGHT_CPI_SIGNER`: Your program's CPI signer PDA defined in Constants.
|
|
|
| 1 |
Light System Program |
Verifies validity proofs, compressed account ownership checks, and CPIs the Account Compression Program to update tree accounts. |
| 2 |
CPI 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.
|
| 3 |
Registered Program PDA |
Provides access control to the Account Compression Program. |
| 4 |
Account Compression Authority |
Signs CPI calls from the Light System Program to the Account Compression Program. |
| 5 |
Account Compression Program |
* Writes to state and address tree accounts.
* Clients and the Account Compression Program do not interact directly — handled internally.
|
| 6 |
System Program |
Solana System Program used to transfer lamports. |
**Build the CPI instruction**:
* `new_cpi()` initializes the CPI instruction with the `proof` to prove the compressed account exists in the state tree *- defined in the Instruction Data (Step 2).*
* `with_light_account` adds the `LightAccount` wrapper configured to close the account with the zero values *- defined in Step 3*.
* `invoke()` calls the Light System Program with `CpiAccounts`.
```text theme={null}
---
description: Build a program that closes compressed accounts
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Build a program that closes compressed accounts
Context:
- Guide: https://zkcompression.com/compressed-pdas/guides/how-to-close-compressed-accounts
- Skills and resources index: https://zkcompression.com/skill.md
- Crate: light-sdk (LightAccount, CpiAccounts, LightSystemProgramCpi, derive_light_cpi_signer!)
- Anchor example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/close
- Native Rust example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/close
Key SDK API: LightAccount::new_close()
### 1. Index project
- Grep `declare_id|#\[program\]|entrypoint!|Pubkey|AccountInfo|close|delete|remove` across src/
- Glob `**/*.rs` and `**/Cargo.toml` for project structure
- Identify: program ID, existing instructions, account structs, framework (Anchor or native)
- Read Cargo.toml — note existing dependencies and Solana SDK version
- Task subagent (Grep/Read/WebFetch) if project has multiple crates to scan in parallel
### 2. Read references
- WebFetch the guide above — review both Anchor and Native Rust code samples
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: what is the goal? (new program from scratch, add account close to existing program, migrate from regular accounts)
- AskUserQuestion: Anchor or Native Rust framework?
- AskUserQuestion: does the program already have compressed account instructions, or is this the first one?
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan: which files to modify, what code to add, dependency changes
- Follow the guide's step order: Dependencies → Constants → Account Struct → Instruction Data → Close Compressed Account → Light System Program CPI
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- Add deps: Bash `cargo add light-sdk@0.23` (add `anchor_lang@0.31` for Anchor or `solana-program@2.2` + `borsh@0.10` for Native Rust)
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- Bash `cargo build-sbf` or `anchor build`
- Bash `cargo test-sbf` or `anchor test` if tests exist
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work
```
# Full Code Example
The example programs below implement all steps from this guide.
Make sure you have your developer environment set up first.
**Install Solana CLI:**
```bash theme={null}
sh -c "$(curl -sSfL https://release.solana.com/v2.2.15/install)"
```
**Install Anchor CLI:**
```bash theme={null}
cargo install --git https://github.com/coral-xyz/anchor avm --force
avm install latest
avm use latest
```
**Install the Light CLI:**
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
**Verify installation:**
```bash theme={null}
light --version
```
For help with debugging, see the [Error Cheatsheet](/resources/error-cheatsheet).
Find the source code [here](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/close).
````rust theme={null}
#![allow(unexpected_cfgs)]
#![allow(deprecated)]
use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize};
use light_sdk::{
account::LightAccount,
address::v2::derive_address,
cpi::{v2::CpiAccounts, CpiSigner},
derive_light_cpi_signer,
instruction::{account_meta::CompressedAccountMeta, PackedAddressTreeInfo, ValidityProof},
LightDiscriminator, PackedAddressTreeInfoExt,
};
use light_sdk::constants::ADDRESS_TREE_V2;
declare_id!("DzQ3za3DVCpXkXhmZVSrNchwbbSsJXmi9MBc8v5tvZuQ");
pub const LIGHT_CPI_SIGNER: CpiSigner =
derive_light_cpi_signer!("DzQ3za3DVCpXkXhmZVSrNchwbbSsJXmi9MBc8v5tvZuQ");
#[program]
pub mod close {
use super::*;
use light_sdk::cpi::{
v2::LightSystemProgramCpi, InvokeLightSystemProgram, LightCpiInstruction,
};
/// Setup: Create a compressed account
pub fn create_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
address_tree_info: PackedAddressTreeInfo,
output_state_tree_index: u8,
message: String,
) -> Result<()> {
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
let address_tree_pubkey = address_tree_info
.get_tree_pubkey(&light_cpi_accounts)
.map_err(|_| ErrorCode::AccountNotEnoughKeys)?;
if address_tree_pubkey.to_bytes() != ADDRESS_TREE_V2 {
msg!("Invalid address tree");
return Err(ProgramError::InvalidAccountData.into());
}
let (address, address_seed) = derive_address(
&[b"message", ctx.accounts.signer.key().as_ref()],
&address_tree_pubkey,
&crate::ID,
);
let mut my_compressed_account = LightAccount::::new_init(
&crate::ID,
Some(address),
output_state_tree_index,
);
my_compressed_account.owner = ctx.accounts.signer.key();
my_compressed_account.message = message.clone();
msg!(
"Created compressed account with message: {}",
my_compressed_account.message
);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(my_compressed_account)?
.with_new_addresses(&[address_tree_info.into_new_address_params_assigned_packed(address_seed, Some(0))])
.invoke(light_cpi_accounts)?;
Ok(())
}
/// Close compressed account
pub fn close_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
account_meta: CompressedAccountMeta,
current_message: String,
) -> Result<()> {
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
let my_compressed_account = LightAccount::::new_close(
&crate::ID,
&account_meta,
MyCompressedAccount {
owner: ctx.accounts.signer.key(),
message: current_message,
},
)?;
msg!("Close compressed account.");
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(my_compressed_account)?
.invoke(light_cpi_accounts)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct GenericAnchorAccounts<'info> {
#[account(mut)]
pub signer: Signer<'info>,
}
#[event]
#[derive(Clone, Debug, Default, LightDiscriminator)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}```
````
Find the source code [here](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/close).
```rust theme={null}
#![allow(unexpected_cfgs)]
#[cfg(any(test, feature = "test-helpers"))]
pub mod test_helpers;
use borsh::{BorshDeserialize, BorshSerialize};
use light_macros::pubkey;
use light_sdk::{
account::sha::LightAccount,
address::v2::derive_address,
cpi::{
v2::{CpiAccounts, LightSystemProgramCpi},
CpiSigner, InvokeLightSystemProgram, LightCpiInstruction,
},
derive_light_cpi_signer,
error::LightSdkError,
instruction::{account_meta::CompressedAccountMeta, PackedAddressTreeInfo, ValidityProof},
LightDiscriminator,
};
use light_sdk::constants::ADDRESS_TREE_V2;
use light_sdk::PackedAddressTreeInfoExt;
use solana_program::{
account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey,
};
pub const ID: Pubkey = pubkey!("NLusgr6vsEjYDvF6nDxpdrhMUxUC19s4XoyshSrGFVN");
pub const LIGHT_CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("NLusgr6vsEjYDvF6nDxpdrhMUxUC19s4XoyshSrGFVN");
#[cfg(not(feature = "no-entrypoint"))]
entrypoint!(process_instruction);
#[derive(Debug, BorshSerialize, BorshDeserialize)]
pub enum InstructionType {
Create,
Close,
}
#[derive(Debug, BorshSerialize, BorshDeserialize)]
pub struct CreateInstructionData {
pub proof: ValidityProof,
pub address_tree_info: PackedAddressTreeInfo,
pub output_state_tree_index: u8,
pub message: String,
}
#[derive(Debug, BorshSerialize, BorshDeserialize)]
pub struct CloseInstructionData {
pub proof: ValidityProof,
pub account_meta: CompressedAccountMeta,
pub current_message: String,
}
#[derive(Debug, Default, Clone, BorshSerialize, BorshDeserialize, LightDiscriminator)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> Result<(), ProgramError> {
let (instruction_type, rest) = instruction_data
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
match InstructionType::try_from_slice(&[*instruction_type])
.map_err(|_| ProgramError::InvalidInstructionData)?
{
InstructionType::Create => create(accounts, rest)?,
InstructionType::Close => close(accounts, rest)?,
}
Ok(())
}
fn create(accounts: &[AccountInfo], instruction_data: &[u8]) -> Result<(), LightSdkError> {
let instruction_data =
CreateInstructionData::try_from_slice(instruction_data).map_err(|_| LightSdkError::Borsh)?;
let signer = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?;
let light_cpi_accounts = CpiAccounts::new(signer, &accounts[1..], LIGHT_CPI_SIGNER);
let address_tree_pubkey = instruction_data
.address_tree_info
.get_tree_pubkey(&light_cpi_accounts)
.map_err(|_| ProgramError::NotEnoughAccountKeys)?;
if address_tree_pubkey.to_bytes() != ADDRESS_TREE_V2 {
solana_program::msg!("Invalid address tree");
return Err(LightSdkError::ProgramError(ProgramError::InvalidAccountData));
}
let (address, address_seed) = derive_address(
&[b"message", signer.key.as_ref()],
&address_tree_pubkey,
&ID,
);
let new_address_params = instruction_data
.address_tree_info
.into_new_address_params_assigned_packed(address_seed, Some(0));
let mut my_compressed_account = LightAccount::::new_init(
&ID,
Some(address),
instruction_data.output_state_tree_index,
);
my_compressed_account.owner = *signer.key;
my_compressed_account.message = instruction_data.message;
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.with_new_addresses(&[new_address_params])
.invoke(light_cpi_accounts)?;
Ok(())
}
fn close(accounts: &[AccountInfo], instruction_data: &[u8]) -> Result<(), LightSdkError> {
let instruction_data =
CloseInstructionData::try_from_slice(instruction_data).map_err(|_| LightSdkError::Borsh)?;
let (signer, remaining_accounts) = accounts
.split_first()
.ok_or(ProgramError::InvalidAccountData)?;
let cpi_accounts = CpiAccounts::new(signer, remaining_accounts, LIGHT_CPI_SIGNER);
let my_compressed_account = LightAccount::::new_close(
&ID,
&instruction_data.account_meta,
MyCompressedAccount {
owner: *signer.key,
message: instruction_data.current_message,
},
)?;
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.invoke(cpi_accounts)?;
Ok(())
}
```
# Next Steps
# Create Compressed Accounts
Source: https://www.zkcompression.com/compressed-pdas/guides/how-to-create-compressed-accounts
Guide to create compressed accounts in Solana programs with full code examples.
Compressed accounts and addresses are created via CPI to the Light System Program.
* Compressed and regular Solana accounts share the same functionality and are fully composable.
* A compressed account has two identifiers: the account hash and its address (optional). In comparison, regular Solana accounts are identified by their address.
* The account hash is not persistent and changes with every write to the account.
* For Solana PDA like behavior your compressed account needs an address as persistent identifier.
Find [full code examples at the end](/compressed-pdas/guides/how-to-create-compressed-accounts#full-code-example) for Anchor and native Rust.
# Implementation Guide
This guide will cover the components of a Solana program that creates compressed accounts.\
Here is the complete flow:
### Dependencies
Add dependencies to your program.
```toml Anchor theme={null}
[dependencies]
light-sdk = "0.23.0"
anchor_lang = "0.31.1"
```
```toml Native Rust theme={null}
[dependencies]
light-sdk = "0.23.0"
borsh = "0.10.4"
solana-program = "2.2"
```
* The `light-sdk` provides macros, wrappers and CPI interface to create and interact with compressed accounts.
* Add the serialization library (`borsh` for native Rust, or use `AnchorSerialize`).
### Constants
Set program address and derive the CPI authority PDA to call the Light System program.
```rust Anchor theme={null}
declare_id!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");
pub const LIGHT_CPI_SIGNER: CpiSigner =
derive_light_cpi_signer!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");
```
```rust Native Rust theme={null}
pub const ID: Pubkey = pubkey!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");
pub const LIGHT_CPI_SIGNER: CpiSigner =
derive_light_cpi_signer!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");
```
**`CPISigner`** is the configuration struct for CPIs to the Light System Program.
* CPI to the Light System program must be signed with a PDA derived by your program with the seed `b"authority"`
* `derive_light_cpi_signer!` derives the CPI signer PDA for you at compile time.
### Compressed Account
```rust Anchor theme={null}
#[event] // declared as event so that it is part of the idl.
#[derive(
Clone,
Debug,
Default,
LightDiscriminator
)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
```
```rust Native Rust theme={null}
#[derive(
Clone,
Debug,
Default,
BorshSerialize,
BorshDeserialize,
LightDiscriminator
)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
```
Define your compressed account struct and derive
* the standard traits (`Clone`, `Debug`, `Default`),
* `borsh` or `AnchorSerialize` to serialize account data, and
* `LightDiscriminator` to implements a unique type ID (8 bytes) to distinguish account types. The default compressed account layout enforces a discriminator in its *own field*, not the first 8 bytes of the data field.
The traits listed above are required for `LightAccount`. `LightAccount` wraps `my-compressed-account` in Step 7 to set the discriminator and create the compressed account's data.
### Instruction Data
Define the instruction data with the following parameters:
Anchor handles instruction deserialization automatically. Pass the parameters directly to the instruction function:
```rust theme={null}
pub fn create_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
address_tree_info: PackedAddressTreeInfo,
output_state_tree_index: u8,
message: String,
) -> Result<()>
```
Define an instruction data struct that will be deserialized from the instruction data:
```rust Native Rust theme={null}
pub struct CreateInstructionData {
pub proof: ValidityProof,
pub address_tree_info: PackedAddressTreeInfo,
pub output_state_tree_index: u8,
pub message: String,
}
```
1. **Validity Proof**
* Define `proof` to include the proof that the address does not exist yet in the specified address tree.
* Clients fetch a validity proof with `getValidityProof()` from an RPC provider that supports ZK Compression (Helius, Triton, ...).
2. **Specify Merkle trees to store address and account hash**
* Define `address_tree_info: PackedAddressTreeInfo` to reference the address tree account used to derive the address in the next step.
* Define `output_state_tree_index` to reference the state tree account that stores the compressed account hash.
Clients pack accounts into the accounts array to reduce transaction size. Packed structs like `PackedAddressTreeInfo` contain account indices (u8) instead of 32 byte pubkeys. The indices point to the account in the accounts array to retrieve the public key and other metadata.
3. **Initial account data**
* Define fields for your program logic. Clients pass the initial values.
* This example includes the `message` field to define the initial state of the account.
### Derive Address
Derive the address as a persistent unique identifier for the compressed account.
```rust Anchor theme={null}
let (address, address_seed) = derive_address(
&[b"message", ctx.accounts.signer.key().as_ref()],
&address_tree_info
.get_tree_pubkey(&light_cpi_accounts)
&crate::ID,
);
```
```rust Native Rust theme={null}
let (address, address_seed) = derive_address(
&[b"message", signer.key.as_ref()],
&instruction_data
.address_tree_info
.get_tree_pubkey(&light_cpi_accounts)
&ID,
);
```
**Pass these parameters to `derive_address()`:**
* `&custom_seeds`: Predefined inputs, such as strings, numbers or other account addresses. This example uses `b"message"` and the signer's pubkey.
* `&address_tree_pubkey`: The pubkey of the address tree where the address will be created.
* Retrieved by calling `get_tree_pubkey()` on `address_tree_info`, which unpacks the index from the accounts array.
* This parameter ensures an address is unique to an address tree. Different trees produce different addresses from identical seeds.
* `&program_id`: Your program's ID.
**The SDK returns:**
* `address`: The derived address for the compressed account.
* `address_seed`: Pass this to the Light System Program CPI in *Step 8* to create the address.
### Address Tree Check (optional)
Ensure global uniqueness of an address by verifying that the address tree pubkey matches the program's tree constant.
Every address is unique, but the same seeds can be used to create different addresses in different address trees. To enforce that a compressed PDA can only be created once with the same seed, you must check the address tree pubkey.
```rust theme={null}
let address_tree = light_cpi_accounts.tree_pubkeys().unwrap()
[address_tree_info.address_merkle_tree_pubkey_index as usize];
if address_tree != light_sdk::constants::ADDRESS_TREE_V2 {
return Err(ProgramError::InvalidAccountData.into());
}
```
### Initialize Compressed Account
Initialize the compressed account struct with `LightAccount::new_init()`.
`new_init()` creates a `LightAccount` instance similar to anchor `Account` and lets your program define the initial account data.
```rust Anchor theme={null}
let mut my_compressed_account = LightAccount::::new_init(
&crate::ID,
Some(address),
output_state_tree_index,
);
my_compressed_account.owner = ctx.accounts.signer.key();
my_compressed_account.message = message.clone();
```
```rust Native Rust theme={null}
let mut my_compressed_account = LightAccount::::new_init(
&ID,
Some(address),
instruction_data.output_state_tree_index,
);
my_compressed_account.owner = *signer.key;
my_compressed_account.message = instruction_data.message;
```
**Pass these parameters to `new_init()`:**
* `&owner`: The program's ID that owns the compressed account.
* `Some(address)`: The derived address from *Step 5*. Pass `None` for accounts without addresses.
* `output_state_tree_index`: References the state tree account that will store the updated account hash, defined in instruction data (*Step 4*)
**The SDK creates:**
* A `LightAccount` wrapper similar to Anchor's `Account.`
* `new_init()` lets the program set the initial data. This example sets:
* `owner` to the signer's pubkey
* `message` to an arbitrary string
### Light System Program CPI
Invoke the Light System Program to create the compressed account and its address.
The Light System Program
* verifies the validity proof against the address tree's Merkle root,
* inserts the address into the address tree, and
* appends the new account hash to the state tree.
```rust theme={null}
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
let new_address_params = address_tree_info
.into_new_address_params_assigned_packed(address_seed, Some(0));
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(my_compressed_account)?
.with_new_addresses(&[new_address_params])
.invoke(light_cpi_accounts)?;
```
**Set up `CpiAccounts::new()`:**
`CpiAccounts::new()` parses accounts for the CPI call to Light System Program.
**Pass these parameters:**
* `ctx.accounts.signer.as_ref()`: the transaction signer
* `ctx.remaining_accounts`: Slice with `[system_accounts, ...packed_tree_accounts]`. The client builds this with `PackedAccounts` and passes it to the instruction.
* `&LIGHT_CPI_SIGNER`: Your program's CPI signer PDA defined in Constants.
```rust theme={null}
let signer = accounts.first();
let light_cpi_accounts = CpiAccounts::new(
signer,
&accounts[1..],
LIGHT_CPI_SIGNER
);
let new_address_params = instruction_data
.address_tree_info
.into_new_address_params_assigned_packed(address_seed, Some(0));
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.with_new_addresses(&[new_address_params])
.invoke(light_cpi_accounts)?;
```
**Set up `CpiAccounts::new()`:**
`CpiAccounts::new()` parses accounts for the CPI call to Light System Program.
**Pass these parameters:**
* `signer`: account that signs and pays for the transaction
* `&accounts[1..]`: Slice with `[system_accounts, ...packed_tree_accounts]`. The client builds this with `PackedAccounts`.
* `&LIGHT_CPI_SIGNER`: Your program's CPI signer PDA defined in Constants.
|
|
|
| 1 |
Light System Program |
Verifies validity proofs, compressed account ownership checks, and CPIs the Account Compression Program to update tree accounts. |
| 2 |
CPI 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.
|
| 3 |
Registered Program PDA |
Provides access control to the Account Compression Program. |
| 4 |
Account Compression Authority |
Signs CPI calls from the Light System Program to the Account Compression Program. |
| 5 |
Account Compression Program |
* Writes to state and address tree accounts.
* Clients and the Account Compression Program do not interact directly — handled internally.
|
| 6 |
System Program |
Solana System Program used to transfer lamports. |
**Build the CPI instruction**:
`new_cpi()` initializes the CPI instruction with the `proof` to prove that an address does not exist yet in the specified address tree *- defined in the Instruction Data (Step 4).*
* `with_light_account` adds the `LightAccount` with the initial compressed account data to the CPI instruction *- defined in Step 7*.
* `with_new_addresses` adds the `address_seed` and metadata to the CPI instruction data - returned by `derive_address` *in Step 5*.
* `invoke(light_cpi_accounts)` calls the Light System Program with `CpiAccounts.`
```text theme={null}
---
description: Build a program that creates compressed accounts with addresses
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Build a program that creates compressed accounts with addresses
Context:
- Guide: https://zkcompression.com/compressed-pdas/guides/how-to-create-compressed-accounts
- Skills and resources index: https://zkcompression.com/skill.md
- Crate: light-sdk (LightAccount, CpiAccounts, LightSystemProgramCpi, derive_light_cpi_signer!)
- Anchor example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/create
- Native Rust example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/create
Key SDK API: LightAccount::new_init(), derive_address()
### 1. Index project
- Grep `declare_id|#\[program\]|entrypoint!|Pubkey|AccountInfo|seeds|init|payer|space` across src/
- Glob `**/*.rs` and `**/Cargo.toml` for project structure
- Identify: program ID, existing instructions, account structs, framework (Anchor or native)
- Read Cargo.toml — note existing dependencies and Solana SDK version
- Task subagent (Grep/Read/WebFetch) if project has multiple crates to scan in parallel
### 2. Read references
- WebFetch the guide above — review both Anchor and Native Rust code samples
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: what is the goal? (new program from scratch, add account creation to existing program, migrate from regular accounts)
- AskUserQuestion: Anchor or Native Rust framework?
- AskUserQuestion: does the program already have compressed account instructions, or is this the first one?
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan: which files to modify, what code to add, dependency changes
- Follow the guide's step order: Dependencies → Constants → Account Struct → Instruction Data → Derive Address → Address Tree Check → Initialize Account → Light System Program CPI
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- Add deps: Bash `cargo add light-sdk@0.23` (add `anchor_lang@0.31` for Anchor or `solana-program@2.2` + `borsh@0.10` for Native Rust)
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- Bash `cargo build-sbf` or `anchor build`
- Bash `cargo test-sbf` or `anchor test` if tests exist
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work
```
# Full Code Example
The example programs below implement all steps from this guide.
**Install Solana CLI:**
```bash theme={null}
sh -c "$(curl -sSfL https://release.solana.com/v2.2.15/install)"
```
**Install Anchor CLI:**
```bash theme={null}
cargo install --git https://github.com/coral-xyz/anchor avm --force
avm install latest
avm use latest
```
**Install the Light CLI:**
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
**Verify installation:**
```bash theme={null}
light --version
```
For help with debugging, see the [Error Cheatsheet](/resources/error-cheatsheet).
Find the source code [here](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/create).
```rust theme={null}
#![allow(unexpected_cfgs)]
#![allow(deprecated)]
use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize};
use light_sdk::{
account::LightAccount,
address::v2::derive_address,
cpi::{v2::CpiAccounts, CpiSigner},
derive_light_cpi_signer,
instruction::{PackedAddressTreeInfo, ValidityProof},
LightDiscriminator, PackedAddressTreeInfoExt,
};
use light_sdk::constants::ADDRESS_TREE_V2;
declare_id!("Hps5oaKdYWqjVZJnAxUE1uwbozwEgZZGCRA57p2wdqcS");
pub const LIGHT_CPI_SIGNER: CpiSigner =
derive_light_cpi_signer!("Hps5oaKdYWqjVZJnAxUE1uwbozwEgZZGCRA57p2wdqcS");
#[program]
pub mod create {
use super::*;
use light_sdk::cpi::{
v2::LightSystemProgramCpi, InvokeLightSystemProgram, LightCpiInstruction,
};
/// Creates a new compressed account
pub fn create_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
address_tree_info: PackedAddressTreeInfo,
output_state_tree_index: u8,
message: String,
) -> Result<()> {
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
let address_tree_pubkey = address_tree_info
.get_tree_pubkey(&light_cpi_accounts)
.map_err(|_| ErrorCode::AccountNotEnoughKeys)?;
if address_tree_pubkey.to_bytes() != ADDRESS_TREE_V2 {
msg!("Invalid address tree");
return Err(ProgramError::InvalidAccountData.into());
}
let (address, address_seed) = derive_address(
&[b"message", ctx.accounts.signer.key().as_ref()],
&address_tree_pubkey,
&crate::ID,
);
let mut my_compressed_account = LightAccount::::new_init(
&crate::ID,
Some(address),
output_state_tree_index,
);
my_compressed_account.owner = ctx.accounts.signer.key();
my_compressed_account.message = message.clone();
msg!(
"Created compressed account with message: {}",
my_compressed_account.message
);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(my_compressed_account)?
.with_new_addresses(&[address_tree_info.into_new_address_params_assigned_packed(address_seed, Some(0))])
.invoke(light_cpi_accounts)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct GenericAnchorAccounts<'info> {
#[account(mut)]
pub signer: Signer<'info>,
}
// declared as event so that it is part of the idl.
#[event]
#[derive(Clone, Debug, Default, LightDiscriminator)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
```
Find the source code [here](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/create).
```rust theme={null}
#![allow(unexpected_cfgs)]
#[cfg(any(test, feature = "test-helpers"))]
pub mod test_helpers;
use borsh::{BorshDeserialize, BorshSerialize};
use light_macros::pubkey;
use light_sdk::{
account::sha::LightAccount,
address::v2::derive_address,
cpi::{
v2::{CpiAccounts, LightSystemProgramCpi},
CpiSigner, InvokeLightSystemProgram, LightCpiInstruction,
},
derive_light_cpi_signer,
error::LightSdkError,
instruction::{PackedAddressTreeInfo, ValidityProof},
LightDiscriminator,
};
use light_sdk::constants::ADDRESS_TREE_V2;
use light_sdk::PackedAddressTreeInfoExt;
use solana_program::{
account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey,
};
pub const ID: Pubkey = pubkey!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");
pub const LIGHT_CPI_SIGNER: CpiSigner =
derive_light_cpi_signer!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");
entrypoint!(process_instruction);
#[repr(u8)]
#[derive(Debug)]
pub enum InstructionType {
Create = 0,
}
impl TryFrom for InstructionType {
type Error = LightSdkError;
fn try_from(value: u8) -> Result {
match value {
0 => Ok(InstructionType::Create),
_ => panic!("Invalid instruction discriminator."),
}
}
}
#[derive(Debug, Default, Clone, BorshSerialize, BorshDeserialize, LightDiscriminator)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct CreateInstructionData {
pub proof: ValidityProof,
pub address_tree_info: PackedAddressTreeInfo,
pub output_state_tree_index: u8,
pub message: String,
}
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> Result<(), ProgramError> {
if program_id != &ID {
return Err(ProgramError::IncorrectProgramId);
}
if instruction_data.is_empty() {
return Err(ProgramError::InvalidInstructionData);
}
let discriminator = InstructionType::try_from(instruction_data[0])
.map_err(|_| ProgramError::InvalidInstructionData)?;
match discriminator {
InstructionType::Create => {
let instruction_data = CreateInstructionData::try_from_slice(&instruction_data[1..])
.map_err(|_| ProgramError::InvalidInstructionData)?;
create(accounts, instruction_data)
}
}
}
pub fn create(
accounts: &[AccountInfo],
instruction_data: CreateInstructionData,
) -> Result<(), ProgramError> {
let signer = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?;
let light_cpi_accounts = CpiAccounts::new(signer, &accounts[1..], LIGHT_CPI_SIGNER);
let address_tree_pubkey = instruction_data
.address_tree_info
.get_tree_pubkey(&light_cpi_accounts)
.map_err(|_| ProgramError::NotEnoughAccountKeys)?;
if address_tree_pubkey.to_bytes() != ADDRESS_TREE_V2 {
solana_program::msg!("Invalid address tree");
return Err(ProgramError::InvalidAccountData);
}
let (address, address_seed) = derive_address(
&[b"message", signer.key.as_ref()],
&address_tree_pubkey,
&ID,
);
let new_address_params = instruction_data
.address_tree_info
.into_new_address_params_assigned_packed(address_seed, Some(0));
let mut my_compressed_account = LightAccount::::new_init(
&ID,
Some(address),
instruction_data.output_state_tree_index,
);
my_compressed_account.owner = *signer.key;
my_compressed_account.message = instruction_data.message;
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.with_new_addresses(&[new_address_params])
.invoke(light_cpi_accounts)?;
Ok(())
}
```
# Next Steps
# Reinitialize Compressed Accounts
Source: https://www.zkcompression.com/compressed-pdas/guides/how-to-reinitialize-compressed-accounts
Guide to reinitialize compressed accounts in Solana programs with full code examples.
Compressed accounts are reinitialized via CPI to the Light System Program.
An empty compressed account can be reinitialized
* with an account hash marked as empty with zero values and zero discriminator
* to create a new account hash at the same address with new values.
Find [full code examples at the end](/compressed-pdas/guides/how-to-reinitialize-compressed-accounts#full-code-example) for Anchor and native Rust.
# Implementation Guide
This guide will cover the components of a Solana program that reinitializes compressed accounts.
Here is the complete flow to reinitialize compressed accounts:
### Program Setup
**Dependencies**
Add dependencies to your program.
```toml Anchor theme={null}
[dependencies]
light-sdk = "0.23.0"
anchor_lang = "0.31.1"
```
```toml Native Rust theme={null}
[dependencies]
light-sdk = "0.23.0"
borsh = "0.10.4"
solana-program = "2.2"
```
* The `light-sdk` provides macros, wrappers and CPI interface to create and interact with compressed accounts.
* Add the serialization library (`borsh` for native Rust, or use `AnchorSerialize`).
**Constants**
Set program address and derive the CPI authority PDA to call the Light System program.
```rust theme={null}
declare_id!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");
pub const LIGHT_CPI_SIGNER: CpiSigner =
derive_light_cpi_signer!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");
```
**`CPISigner`** is the configuration struct for CPI's to the Light System Program.
* CPIs to the Light System program must be signed with a PDA derived by your program with the seed `b"authority"`
* `derive_light_cpi_signer!` derives the CPI signer PDA for you at compile time.
**Compressed Account**
Define your compressed account struct.
```rust Anchor theme={null}
#[event] // declared as event so that it is part of the idl.
#[derive(
Clone,
Debug,
Default,
LightDiscriminator
)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
```
```rust Native Rust theme={null}
#[derive(
Debug,
Default,
Clone,
BorshSerialize,
BorshDeserialize,
LightDiscriminator,
)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
```
You derive
* the standard traits (`Clone`, `Debug`, `Default`),
* `borsh` or `AnchorSerialize` to serialize account data, and
* `LightDiscriminator` to implements a unique type ID (8 bytes) to distinguish account types. The default compressed account layout enforces a discriminator in its *own field*, not the first 8 bytes of the data field.
The traits listed above are required for `LightAccount`. `LightAccount` wraps `MyCompressedAccount` in Step 3 to set the discriminator and create the compressed account's data.
### Instruction Data
Define the instruction data with the following parameters:
```rust theme={null}
pub fn reinit_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
account_meta: CompressedAccountMeta,
) -> Result<()>
```
```rust theme={null}
pub struct ReinitInstructionData {
pub proof: ValidityProof,
pub account_meta: CompressedAccountMeta,
}
```
1. **Validity Proof**
* Define `proof` to include the proof that the closed account with zero values exists in the state tree.
* Clients fetch a validity proof with `getValidityProof()` from an RPC provider that supports ZK Compression (Helius, Triton, ...).
2. **Specify input state and output state tree (stores new account hash)**
* Define `account_meta: CompressedAccountMeta` to reference the closed account and specify the state tree to store the new account hash:
* `tree_info: PackedStateTreeInfo`: References the existing account hash in the state tree.
* `address`: The account's derived address.
* `output_state_tree_index`: References the state tree account that will store the new compressed account hash.
Reinitialization does not require `current_value` parameters. `new_empty()` automatically uses the closed account as input.
### Reinitialize Closed Account
Reinitialize the closed account with `LightAccount::new_empty()`.
`new_empty()`
1. reconstructs the closed account hash with zero values as input, and
2. creates output state with default-initialized values.
You can set custom values in the same transaction:
1. Reinitialize with `new_empty()`, and
2. Update with `new_mut()` to set custom values.
```rust theme={null}
let my_compressed_account = LightAccount::::new_empty(
&crate::ID,
&account_meta,
)?;
```
```rust theme={null}
let my_compressed_account = LightAccount::::new_empty(
&ID,
&instruction_data.account_meta,
)?;
```
**Pass these parameters to `new_empty()`:**
* `&program_id`: The program's ID that owns the compressed account.
* `&account_meta`: The `CompressedAccountMeta` from instruction data (*Step 2*) that identifies the existing account and specifies the output state tree.
* Anchor: Pass `account_meta` by reference. It is automatically deserialized as a function parameter.
* Native Rust: Manually deserialize the instruction data struct, then pass the field by reference `&instruction_data.account_meta`.
**The SDK creates:**
* A `LightAccount` wrapper with account data automatically initialized to default values using the `Default` trait.
* This creates a zero-initialized instance: `Pubkey` as all zeros, `u64` as `0`, `String` as empty.
`new_empty()` reconstructs the closed account hash with zero values. The Light System Program verifies the closed account hash and creates the output hash in *Step 4*.
### Light System Program CPI
Invoke the Light System Program to reinitialize the compressed account.
The Light System Program
* validates the closed account hash exists in state tree,
* nullifies the closed account hash, and
* appends the new account hash with provided values to the state tree.
```rust theme={null}
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(my_compressed_account)?
.invoke(light_cpi_accounts)?;
```
**Set up `CpiAccounts::new()`:**
`CpiAccounts::new()` parses accounts for the CPI call to Light System Program.
**Pass these parameters:**
* `ctx.accounts.signer.as_ref()`: the transaction signer
* `ctx.remaining_accounts`: Slice with `[system_accounts, ...packed_tree_accounts]`. The client builds this with `PackedAccounts` and passes it to the instruction.
* `&LIGHT_CPI_SIGNER`: Your program's CPI signer PDA defined in Constants.
```rust theme={null}
let (signer, remaining_accounts) = accounts
.split_first();
let cpi_accounts = CpiAccounts::new(
signer,
remaining_accounts,
LIGHT_CPI_SIGNER
);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.invoke(cpi_accounts)?;
```
**Set up `CpiAccounts::new()`:**
`CpiAccounts::new()` parses accounts for the CPI call to Light System Program.
**Pass these parameters:**
* `signer`: account that signs and pays for the transaction
* `remaining_accounts`: Slice with `[system_accounts, ...packed_tree_accounts]`. The client builds this with `PackedAccounts`.
* `split_first()` extracts the fee payer from the accounts array to separate it from the Light System Program accounts needed for the CPI.
* `&LIGHT_CPI_SIGNER`: Your program's CPI signer PDA defined in Constants.
|
|
|
| 1 |
Light System Program |
Verifies validity proofs, compressed account ownership checks, and CPIs the Account Compression Program to update tree accounts. |
| 2 |
CPI 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.
|
| 3 |
Registered Program PDA |
Provides access control to the Account Compression Program. |
| 4 |
Account Compression Authority |
Signs CPI calls from the Light System Program to the Account Compression Program. |
| 5 |
Account Compression Program |
* Writes to state and address tree accounts.
* Clients and the Account Compression Program do not interact directly — handled internally.
|
| 6 |
System Program |
Solana System Program used to transfer lamports. |
**Build the CPI instruction**:
* `new_cpi()` initializes the CPI instruction with the `proof` to prove the closed account hash exists in the state tree *- defined in the Instruction Data (Step 2).*
* `with_light_account` adds the `LightAccount` configured with the closed account hash as input and provided values as output *- defined in Step 3*.
* `invoke(light_cpi_accounts)` calls the Light System Program with `CpiAccounts`.
```text theme={null}
---
description: Build a program that reinitializes closed compressed accounts
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Build a program that reinitializes closed compressed accounts
Context:
- Guide: https://zkcompression.com/compressed-pdas/guides/how-to-reinitialize-compressed-accounts
- Skills and resources index: https://zkcompression.com/skill.md
- Crate: light-sdk (LightAccount, CpiAccounts, LightSystemProgramCpi, derive_light_cpi_signer!)
- Anchor example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/reinit
- Native Rust example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/reinit
Key SDK API: LightAccount::new_empty()
### 1. Index project
- Grep `declare_id|#\[program\]|entrypoint!|Pubkey|AccountInfo|reinit|empty|reset` across src/
- Glob `**/*.rs` and `**/Cargo.toml` for project structure
- Identify: program ID, existing instructions, account structs, framework (Anchor or native)
- Read Cargo.toml — note existing dependencies and Solana SDK version
- Task subagent (Grep/Read/WebFetch) if project has multiple crates to scan in parallel
### 2. Read references
- WebFetch the guide above — review both Anchor and Native Rust code samples
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: what is the goal? (new program from scratch, add account reinitialization to existing program, migrate from regular accounts)
- AskUserQuestion: Anchor or Native Rust framework?
- AskUserQuestion: does the program already have compressed account instructions, or is this the first one?
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan: which files to modify, what code to add, dependency changes
- Follow the guide's step order: Dependencies → Constants → Account Struct → Instruction Data → Reinitialize Closed Account → Light System Program CPI
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- Add deps: Bash `cargo add light-sdk@0.23` (add `anchor_lang@0.31` for Anchor or `solana-program@2.2` + `borsh@0.10` for Native Rust)
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- Bash `cargo build-sbf` or `anchor build`
- Bash `cargo test-sbf` or `anchor test` if tests exist
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work
```
# Full Code Example
The counter programs below implement all steps from this guide.
**Install Solana CLI:**
```bash theme={null}
sh -c "$(curl -sSfL https://release.solana.com/v2.2.15/install)"
```
**Install Anchor CLI:**
```bash theme={null}
cargo install --git https://github.com/coral-xyz/anchor avm --force
avm install latest
avm use latest
```
**Install the Light CLI:**
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
**Verify installation:**
```bash theme={null}
light --version
```
For help with debugging, see the [Error Cheatsheet](/resources/error-cheatsheet).
Find the source code [here](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/reinit).
````rust theme={null}
#![allow(unexpected_cfgs)]
#![allow(deprecated)]
use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize};
use light_sdk::{
account::LightAccount,
address::v2::derive_address,
cpi::{v2::CpiAccounts, CpiSigner},
derive_light_cpi_signer,
instruction::{account_meta::CompressedAccountMeta, PackedAddressTreeInfo, ValidityProof},
LightDiscriminator, PackedAddressTreeInfoExt,
};
use light_sdk::constants::ADDRESS_TREE_V2;
declare_id!("DeSUZ4to3qN7mQimoTgvEnBXiBWeTqMVcMz3ynLaWx1t");
pub const LIGHT_CPI_SIGNER: CpiSigner =
derive_light_cpi_signer!("DeSUZ4to3qN7mQimoTgvEnBXiBWeTqMVcMz3ynLaWx1t");
#[program]
pub mod reinit {
use super::*;
use light_sdk::cpi::{
v2::LightSystemProgramCpi, InvokeLightSystemProgram, LightCpiInstruction,
};
/// Setup: Create a compressed account
pub fn create_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
address_tree_info: PackedAddressTreeInfo,
output_state_tree_index: u8,
message: String,
) -> Result<()> {
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
let address_tree_pubkey = address_tree_info
.get_tree_pubkey(&light_cpi_accounts)
.map_err(|_| ErrorCode::AccountNotEnoughKeys)?;
if address_tree_pubkey.to_bytes() != ADDRESS_TREE_V2 {
msg!("Invalid address tree");
return Err(ProgramError::InvalidAccountData.into());
}
let (address, address_seed) = derive_address(
&[b"message", ctx.accounts.signer.key().as_ref()],
&address_tree_pubkey,
&crate::ID,
);
let mut my_compressed_account = LightAccount::::new_init(
&crate::ID,
Some(address),
output_state_tree_index,
);
my_compressed_account.owner = ctx.accounts.signer.key();
my_compressed_account.message = message.clone();
msg!(
"Created compressed account with message: {}",
my_compressed_account.message
);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(my_compressed_account)?
.with_new_addresses(&[address_tree_info.into_new_address_params_assigned_packed(address_seed, Some(0))])
.invoke(light_cpi_accounts)?;
Ok(())
}
/// Setup: Close compressed account
pub fn close_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
account_meta: CompressedAccountMeta,
current_message: String,
) -> Result<()> {
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
let my_compressed_account = LightAccount::::new_close(
&crate::ID,
&account_meta,
MyCompressedAccount {
owner: ctx.accounts.signer.key(),
message: current_message,
},
)?;
msg!("Close compressed account.");
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(my_compressed_account)?
.invoke(light_cpi_accounts)?;
Ok(())
}
/// Reinitialize closed compressed account
pub fn reinit_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
account_meta: CompressedAccountMeta,
) -> Result<()> {
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
let my_compressed_account = LightAccount::::new_empty(
&crate::ID,
&account_meta,
)?;
msg!("Reinitializing closed compressed account");
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(my_compressed_account)?
.invoke(light_cpi_accounts)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct GenericAnchorAccounts<'info> {
#[account(mut)]
pub signer: Signer<'info>,
}
#[event]
#[derive(Clone, Debug, Default, LightDiscriminator)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}```
````
Find the source code [here](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/reinit).
```rust theme={null}
#![allow(unexpected_cfgs)]
#[cfg(any(test, feature = "test-helpers"))]
pub mod test_helpers;
use borsh::{BorshDeserialize, BorshSerialize};
use light_macros::pubkey;
use light_sdk::{
account::sha::LightAccount,
address::v2::derive_address,
cpi::{
v2::{CpiAccounts, LightSystemProgramCpi},
CpiSigner, InvokeLightSystemProgram, LightCpiInstruction,
},
derive_light_cpi_signer,
error::LightSdkError,
instruction::{account_meta::CompressedAccountMeta, PackedAddressTreeInfo, ValidityProof},
LightDiscriminator,
};
use light_sdk::constants::ADDRESS_TREE_V2;
use light_sdk::PackedAddressTreeInfoExt;
use solana_program::{
account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey,
};
pub const ID: Pubkey = pubkey!("C9WiPUaQ5PRjEWg7vUmgekfuQtAgFZFhn12ytXEMDr8y");
pub const LIGHT_CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("C9WiPUaQ5PRjEWg7vUmgekfuQtAgFZFhn12ytXEMDr8y");
#[cfg(not(feature = "no-entrypoint"))]
entrypoint!(process_instruction);
#[derive(Debug, BorshSerialize, BorshDeserialize)]
pub enum InstructionType {
Create,
Close,
Reinit,
}
#[derive(Debug, BorshSerialize, BorshDeserialize)]
pub struct CreateInstructionData {
pub proof: ValidityProof,
pub address_tree_info: PackedAddressTreeInfo,
pub output_state_tree_index: u8,
pub message: String,
}
#[derive(Debug, BorshSerialize, BorshDeserialize)]
pub struct CloseInstructionData {
pub proof: ValidityProof,
pub account_meta: CompressedAccountMeta,
pub current_message: String,
}
#[derive(Debug, BorshSerialize, BorshDeserialize)]
pub struct ReinitInstructionData {
pub proof: ValidityProof,
pub account_meta: CompressedAccountMeta,
}
#[derive(Debug, Default, Clone, BorshSerialize, BorshDeserialize, LightDiscriminator)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> Result<(), ProgramError> {
let (instruction_type, rest) = instruction_data
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
match InstructionType::try_from_slice(&[*instruction_type])
.map_err(|_| ProgramError::InvalidInstructionData)?
{
InstructionType::Create => create(accounts, rest)?,
InstructionType::Close => close(accounts, rest)?,
InstructionType::Reinit => reinit(accounts, rest)?,
}
Ok(())
}
fn create(accounts: &[AccountInfo], instruction_data: &[u8]) -> Result<(), LightSdkError> {
let instruction_data =
CreateInstructionData::try_from_slice(instruction_data).map_err(|_| LightSdkError::Borsh)?;
let signer = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?;
let light_cpi_accounts = CpiAccounts::new(signer, &accounts[1..], LIGHT_CPI_SIGNER);
let address_tree_pubkey = instruction_data
.address_tree_info
.get_tree_pubkey(&light_cpi_accounts)
.map_err(|_| ProgramError::NotEnoughAccountKeys)?;
if address_tree_pubkey.to_bytes() != ADDRESS_TREE_V2 {
solana_program::msg!("Invalid address tree");
return Err(LightSdkError::ProgramError(ProgramError::InvalidAccountData));
}
let (address, address_seed) = derive_address(
&[b"message", signer.key.as_ref()],
&address_tree_pubkey,
&ID,
);
let new_address_params = instruction_data
.address_tree_info
.into_new_address_params_assigned_packed(address_seed, Some(0));
let mut my_compressed_account = LightAccount::::new_init(
&ID,
Some(address),
instruction_data.output_state_tree_index,
);
my_compressed_account.owner = *signer.key;
my_compressed_account.message = instruction_data.message;
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.with_new_addresses(&[new_address_params])
.invoke(light_cpi_accounts)?;
Ok(())
}
fn close(accounts: &[AccountInfo], instruction_data: &[u8]) -> Result<(), LightSdkError> {
let instruction_data =
CloseInstructionData::try_from_slice(instruction_data).map_err(|_| LightSdkError::Borsh)?;
let (signer, remaining_accounts) = accounts
.split_first()
.ok_or(ProgramError::InvalidAccountData)?;
let cpi_accounts = CpiAccounts::new(signer, remaining_accounts, LIGHT_CPI_SIGNER);
let my_compressed_account = LightAccount::::new_close(
&ID,
&instruction_data.account_meta,
MyCompressedAccount {
owner: *signer.key,
message: instruction_data.current_message,
},
)?;
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.invoke(cpi_accounts)?;
Ok(())
}
fn reinit(accounts: &[AccountInfo], instruction_data: &[u8]) -> Result<(), LightSdkError> {
let instruction_data =
ReinitInstructionData::try_from_slice(instruction_data).map_err(|_| LightSdkError::Borsh)?;
let (signer, remaining_accounts) = accounts
.split_first()
.ok_or(ProgramError::InvalidAccountData)?;
let cpi_accounts = CpiAccounts::new(signer, remaining_accounts, LIGHT_CPI_SIGNER);
let my_compressed_account = LightAccount::::new_empty(
&ID,
&instruction_data.account_meta,
)?;
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.invoke(cpi_accounts)?;
Ok(())
}
```
# Next Steps
# Update Compressed Accounts
Source: https://www.zkcompression.com/compressed-pdas/guides/how-to-update-compressed-accounts
Guide to update compressed accounts in Solana programs with full code examples.
Compressed accounts are updated via CPI to the Light System Program.
The update of a compressed account follows a UTXO pattern, unlike regular Solana accounts that overwrite data in place. Each update of a compressed account
* consumes the existing account hash and
* produces a new account hash with updated data.
* The existing account hash is nullified to prevent double spending.
Find [full code examples at the end](/compressed-pdas/guides/how-to-update-compressed-accounts#full-code-example) for Anchor and native Rust.
# Implementation Guide
This guide will cover the components of a Solana program that updates compressed accounts.\
Here is the complete flow:
### Program Setup
**Dependencies**
Add dependencies to your program.
```toml Anchor theme={null}
[dependencies]
light-sdk = "0.23.0"
anchor_lang = "0.31.1"
```
```toml Native Rust theme={null}
[dependencies]
light-sdk = "0.23.0"
borsh = "0.10.4"
solana-program = "2.2"
```
* The `light-sdk` provides macros, wrappers and CPI interface to create and interact with compressed accounts.
* Add the serialization library (`borsh` for native Rust, or use `AnchorSerialize`).
**Constants**
Set program address and derive the CPI authority PDA to call the Light System program.
```rust theme={null}
declare_id!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");
pub const LIGHT_CPI_SIGNER: CpiSigner =
derive_light_cpi_signer!("rent4o4eAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPq");
```
**`CPISigner`** is the configuration struct for CPI's to the Light System Program.
* CPIs to the Light System program must be signed with a PDA derived by your program with the seed `b"authority"`
* `derive_light_cpi_signer!` derives the CPI signer PDA for you at compile time.
**Compressed Account**
Define your compressed account struct.
```rust Anchor theme={null}
#[event] // declared as event so that it is part of the idl.
#[derive(
Clone,
Debug,
Default,
LightDiscriminator
)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
```
```rust Native Rust theme={null}
#[derive(
Debug,
Default,
Clone,
BorshSerialize,
BorshDeserialize,
LightDiscriminator,
)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
```
You derive
* the standard traits (`Clone`, `Debug`, `Default`),
* `borsh` or `AnchorSerialize` to serialize account data, and
* `LightDiscriminator` to implements a unique type ID (8 bytes) to distinguish account types. The default compressed account layout enforces a discriminator in its *own field*, not the first 8 bytes of the data field.
The traits listed above are required for `LightAccount`. `LightAccount` wraps `MyCompressedAccount` in Step 3 to set the discriminator and create the compressed account's data.
### Instruction Data
Define the instruction data with the following parameters:
Anchor handles instruction deserialization automatically. Pass the parameters directly to the instruction function:
```rust theme={null}
pub fn update_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
current_account: MyCompressedAccount,
account_meta: CompressedAccountMeta,
new_message: String,
) -> Result<()>
```
```rust theme={null}
pub struct UpdateInstructionData {
pub proof: ValidityProof,
pub account_meta: CompressedAccountMeta,
pub current_message: String,
pub new_message: String,
}
```
1. **Validity Proof**
* Define `proof` to include the proof that the account exists in the state tree.
* Clients fetch a validity proof with `getValidityProof()` from an RPC provider that supports ZK Compression (Helius, Triton, ...).
2. **Specify input state and output state tree (stores updated account hash)**
* Define `account_meta: CompressedAccountMeta` to reference the existing account and specify the state tree to store the updated account hash:
* `tree_info: PackedStateTreeInfo`: References the existing account hash in the state tree.
* `address`: The account's derived address.
* `output_state_tree_index`: References the state tree account that will store the updated account hash.
Clients fetch the current account with `getCompressedAccount()` and populate `CompressedAccountMeta` with the account's metadata.
3. **Current account data**
* Define fields to include the current account data passed by the client.
* This depends on your program logic. This example includes `current_message` (or `current_account` in Anchor) and `new_message` fields.
* `new_message` contains the new data that will replace the message field of the compressed account after the update.
### Update Compressed Account
Load the compressed account and update it with `LightAccount::new_mut()`.
`new_mut()`:
* hashes the current account data as input state and
* lets your program define the output state.
```rust theme={null}
let mut my_compressed_account = LightAccount::::new_mut(
&crate::ID,
&account_meta,
current_account,
)?;
my_compressed_account.message = new_message.clone();
```
```rust theme={null}
let mut my_compressed_account = LightAccount::::new_mut(
&ID,
&instruction_data.account_meta,
MyCompressedAccount {
owner: *signer.key,
message: instruction_data.current_message,
},
)?;
my_compressed_account.account.message = instruction_data.new_message;
```
**Pass these parameters to `new_mut()`:**
* `&program_id`: The program's ID that owns the compressed account.
* `&account_meta`: The `CompressedAccountMeta` from instruction data (*Step 2*) that identifies the existing account and specifies the output state tree.
* Include the curent account data.
* Anchor: Pass `current_account` directly
* Native: Construct `MyCompressedAccount` with data from `instruction_data`
**The SDK creates:**
* A `LightAccount` wrapper similar to Anchor's `Account`.
* `new_mut()` lets the program modify the output state. This example sets `message` to `new_message`.
`new_mut()` only hashes the input state. The Light System Program verifies that input hash exists in a state tree and creates the output hash in *Step 4.*
### Light System Program CPI
Invoke the Light System Program to update the compressed account.
The Light System Program
* validates the account exists in state tree,
* nullifies the existing account hash in the state tree, and
* appends the updated account hash to the state tree.
```rust theme={null}
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(my_compressed_account)?
.invoke(light_cpi_accounts)?;
```
**Set up `CpiAccounts::new()`:**
`CpiAccounts::new()` parses accounts for the CPI call to Light System Program.
**Pass these parameters:**
* `ctx.accounts.signer.as_ref()`: the transaction signer
* `ctx.remaining_accounts`: Slice with `[system_accounts, ...packed_tree_accounts]`. The client builds this with `PackedAccounts` and passes it to the instruction.
* `&LIGHT_CPI_SIGNER`: Your program's CPI signer PDA defined in Constants.
```rust theme={null}
let signer = accounts.first();
let light_cpi_accounts = CpiAccounts::new(
signer,
&accounts[1..],
LIGHT_CPI_SIGNER
);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.invoke(light_cpi_accounts)?;
```
**Set up `CpiAccounts::new()`:**
`CpiAccounts::new()` parses accounts for the CPI call to Light System Program.
**Pass these parameters:**
* `signer`: account that signs and pays for the transaction
* `&accounts[1..]`: Slice with `[system_accounts, ...packed_tree_accounts]`. The client builds this with `PackedAccounts`.
* `&LIGHT_CPI_SIGNER`: Your program's CPI signer PDA defined in Constants.
|
|
|
| 1 |
Light System Program |
Verifies validity proofs, compressed account ownership checks, and CPIs the Account Compression Program to update tree accounts. |
| 2 |
CPI 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.
|
| 3 |
Registered Program PDA |
Provides access control to the Account Compression Program. |
| 4 |
Account Compression Authority |
Signs CPI calls from the Light System Program to the Account Compression Program. |
| 5 |
Account Compression Program |
* Writes to state and address tree accounts.
* Clients and the Account Compression Program do not interact directly — handled internally.
|
| 6 |
System Program |
Solana System Program used to transfer lamports. |
**Build the CPI instruction**:
* `new_cpi()` initializes the CPI instruction with the `proof` to prove that the account exists in the specified state tree - *in the Instruction Data (Step 2).*
* `with_light_account` adds the `LightAccount` with the modified compressed account data *- defined in Step 3*
* `invoke(light_cpi_accounts)` calls the Light System Program with `CpiAccounts`.
```text theme={null}
---
description: Build a program that updates compressed accounts
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Build a program that updates compressed accounts
Context:
- Guide: https://zkcompression.com/compressed-pdas/guides/how-to-update-compressed-accounts
- Skills and resources index: https://zkcompression.com/skill.md
- Crate: light-sdk (LightAccount, CpiAccounts, LightSystemProgramCpi, derive_light_cpi_signer!)
- Anchor example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/update
- Native Rust example: https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/update
Key SDK API: LightAccount::new_mut()
### 1. Index project
- Grep `declare_id|#\[program\]|entrypoint!|Pubkey|AccountInfo|mut|update|modify` across src/
- Glob `**/*.rs` and `**/Cargo.toml` for project structure
- Identify: program ID, existing instructions, account structs, framework (Anchor or native)
- Read Cargo.toml — note existing dependencies and Solana SDK version
- Task subagent (Grep/Read/WebFetch) if project has multiple crates to scan in parallel
### 2. Read references
- WebFetch the guide above — review both Anchor and Native Rust code samples
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: what is the goal? (new program from scratch, add account update to existing program, migrate from regular accounts)
- AskUserQuestion: Anchor or Native Rust framework?
- AskUserQuestion: does the program already have compressed account instructions, or is this the first one?
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan: which files to modify, what code to add, dependency changes
- Follow the guide's step order: Dependencies → Constants → Account Struct → Instruction Data → Update Compressed Account → Light System Program CPI
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- Add deps: Bash `cargo add light-sdk@0.23` (add `anchor_lang@0.31` for Anchor or `solana-program@2.2` + `borsh@0.10` for Native Rust)
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- Bash `cargo build-sbf` or `anchor build`
- Bash `cargo test-sbf` or `anchor test` if tests exist
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work
```
# Full Code Example
The example programs below implement all steps from this guide.
**Install Solana CLI:**
```bash theme={null}
sh -c "$(curl -sSfL https://release.solana.com/v2.2.15/install)"
```
**Install Anchor CLI:**
```bash theme={null}
cargo install --git https://github.com/coral-xyz/anchor avm --force
avm install latest
avm use latest
```
**Install the Light CLI:**
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
**Verify installation:**
```bash theme={null}
light --version
```
For help with debugging, see the [Error Cheatsheet](/resources/error-cheatsheet).
Find the source code [here](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/update).
````rust theme={null}
#![allow(unexpected_cfgs)]
#![allow(deprecated)]
use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize};
use light_sdk::{
account::LightAccount,
address::v2::derive_address,
cpi::{v2::CpiAccounts, CpiSigner},
derive_light_cpi_signer,
instruction::{account_meta::CompressedAccountMeta, PackedAddressTreeInfo, ValidityProof},
LightDiscriminator, PackedAddressTreeInfoExt,
};
use light_sdk::constants::ADDRESS_TREE_V2;
declare_id!("Cj3DxyqB7wJh511VKexsjKt7Hx1kvPvCBMrbLuL8grKc");
pub const LIGHT_CPI_SIGNER: CpiSigner =
derive_light_cpi_signer!("Cj3DxyqB7wJh511VKexsjKt7Hx1kvPvCBMrbLuL8grKc");
#[program]
pub mod update {
use super::*;
use light_sdk::cpi::{
v2::LightSystemProgramCpi, InvokeLightSystemProgram, LightCpiInstruction,
};
/// Setup: Creates a compressed account
pub fn create_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
address_tree_info: PackedAddressTreeInfo,
output_state_tree_index: u8,
message: String,
) -> Result<()> {
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
let address_tree_pubkey = address_tree_info
.get_tree_pubkey(&light_cpi_accounts)
.map_err(|_| ErrorCode::AccountNotEnoughKeys)?;
if address_tree_pubkey.to_bytes() != ADDRESS_TREE_V2 {
msg!("Invalid address tree");
return Err(ProgramError::InvalidAccountData.into());
}
let (address, address_seed) = derive_address(
&[b"message", ctx.accounts.signer.key().as_ref()],
&address_tree_pubkey,
&crate::ID,
);
let mut my_compressed_account = LightAccount::::new_init(
&crate::ID,
Some(address),
output_state_tree_index,
);
my_compressed_account.owner = ctx.accounts.signer.key();
my_compressed_account.message = message.clone();
msg!(
"Created compressed account with message: {}",
my_compressed_account.message
);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(my_compressed_account)?
.with_new_addresses(&[address_tree_info.into_new_address_params_assigned_packed(address_seed, Some(0))])
.invoke(light_cpi_accounts)?;
Ok(())
}
/// Updates an existing compressed account's message
pub fn update_account<'info>(
ctx: Context<'_, '_, '_, 'info, GenericAnchorAccounts<'info>>,
proof: ValidityProof,
current_account: MyCompressedAccount,
account_meta: CompressedAccountMeta,
new_message: String,
) -> Result<()> {
let light_cpi_accounts = CpiAccounts::new(
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
crate::LIGHT_CPI_SIGNER,
);
let mut my_compressed_account = LightAccount::::new_mut(
&crate::ID,
&account_meta,
current_account,
)?;
my_compressed_account.message = new_message.clone();
msg!(
"Updated compressed account message to: {}",
my_compressed_account.message
);
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof)
.with_light_account(my_compressed_account)?
.invoke(light_cpi_accounts)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct GenericAnchorAccounts<'info> {
#[account(mut)]
pub signer: Signer<'info>,
}
#[event]
#[derive(Clone, Debug, Default, LightDiscriminator)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}```
````
Find the source code [here](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/update).
```rust theme={null}
#![allow(unexpected_cfgs)]
#[cfg(any(test, feature = "test-helpers"))]
pub mod test_helpers;
use borsh::{BorshDeserialize, BorshSerialize};
use light_macros::pubkey;
use light_sdk::{
account::sha::LightAccount,
address::v2::derive_address,
cpi::{
v2::{CpiAccounts, LightSystemProgramCpi},
CpiSigner, InvokeLightSystemProgram, LightCpiInstruction,
},
derive_light_cpi_signer,
error::LightSdkError,
instruction::{account_meta::CompressedAccountMeta, PackedAddressTreeInfo, ValidityProof},
LightDiscriminator,
};
use light_sdk::constants::ADDRESS_TREE_V2;
use light_sdk::PackedAddressTreeInfoExt;
use solana_program::{
account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey,
};
pub const ID: Pubkey = pubkey!("2m6LXA7E6kMSkK6QHq2WCznD6kvhDcVFqEKpETKAQxYe");
pub const LIGHT_CPI_SIGNER: CpiSigner =
derive_light_cpi_signer!("2m6LXA7E6kMSkK6QHq2WCznD6kvhDcVFqEKpETKAQxYe");
entrypoint!(process_instruction);
#[repr(u8)]
#[derive(Debug)]
pub enum InstructionType {
Create = 0,
Update = 1,
}
impl TryFrom for InstructionType {
type Error = LightSdkError;
fn try_from(value: u8) -> Result {
match value {
0 => Ok(InstructionType::Create),
1 => Ok(InstructionType::Update),
_ => panic!("Invalid instruction discriminator."),
}
}
}
#[derive(
Debug, Default, Clone, BorshSerialize, BorshDeserialize, LightDiscriminator,
)]
pub struct MyCompressedAccount {
pub owner: Pubkey,
pub message: String,
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct CreateInstructionData {
pub proof: ValidityProof,
pub address_tree_info: PackedAddressTreeInfo,
pub output_state_tree_index: u8,
pub message: String,
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct UpdateInstructionData {
pub proof: ValidityProof,
pub account_meta: CompressedAccountMeta,
pub current_message: String,
pub new_message: String,
}
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> Result<(), ProgramError> {
if program_id != &ID {
return Err(ProgramError::IncorrectProgramId);
}
if instruction_data.is_empty() {
return Err(ProgramError::InvalidInstructionData);
}
let discriminator = InstructionType::try_from(instruction_data[0])
.map_err(|_| ProgramError::InvalidInstructionData)?;
match discriminator {
InstructionType::Create => {
let instruction_data =
CreateInstructionData::try_from_slice(&instruction_data[1..])
.map_err(|_| ProgramError::InvalidInstructionData)?;
create(accounts, instruction_data)
}
InstructionType::Update => {
let instruction_data =
UpdateInstructionData::try_from_slice(&instruction_data[1..])
.map_err(|_| ProgramError::InvalidInstructionData)?;
update(accounts, instruction_data)
}
}
}
pub fn create(
accounts: &[AccountInfo],
instruction_data: CreateInstructionData,
) -> Result<(), ProgramError> {
let signer = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?;
let light_cpi_accounts = CpiAccounts::new(signer, &accounts[1..], LIGHT_CPI_SIGNER);
let address_tree_pubkey = instruction_data
.address_tree_info
.get_tree_pubkey(&light_cpi_accounts)
.map_err(|_| ProgramError::NotEnoughAccountKeys)?;
if address_tree_pubkey.to_bytes() != ADDRESS_TREE_V2 {
solana_program::msg!("Invalid address tree");
return Err(ProgramError::InvalidAccountData);
}
let (address, address_seed) = derive_address(
&[b"message", signer.key.as_ref()],
&address_tree_pubkey,
&ID,
);
let new_address_params = instruction_data
.address_tree_info
.into_new_address_params_assigned_packed(address_seed, Some(0));
let mut my_compressed_account = LightAccount::::new_init(
&ID,
Some(address),
instruction_data.output_state_tree_index,
);
my_compressed_account.owner = *signer.key;
my_compressed_account.message = instruction_data.message;
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.with_new_addresses(&[new_address_params])
.invoke(light_cpi_accounts)?;
Ok(())
}
pub fn update(
accounts: &[AccountInfo],
instruction_data: UpdateInstructionData,
) -> Result<(), ProgramError> {
let signer = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?;
let light_cpi_accounts = CpiAccounts::new(signer, &accounts[1..], LIGHT_CPI_SIGNER);
let mut my_compressed_account = LightAccount::::new_mut(
&ID,
&instruction_data.account_meta,
MyCompressedAccount {
owner: *signer.key,
message: instruction_data.current_message,
},
)?;
// Update the account data with new message
my_compressed_account.account.message = instruction_data.new_message;
LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, instruction_data.proof)
.with_light_account(my_compressed_account)?
.invoke(light_cpi_accounts)?;
Ok(())
}
```
# Next Steps
# Create Nullifier PDAs
Source: https://www.zkcompression.com/compressed-pdas/nullifier-pda
For some use cases, such as sending payments, you might want to prevent your on chain instruction from being executed more than once.
The nullifier program utility solves this for you.
1. It derives PDA from `["nullifier", id]` seeds (where `id` is your unique identifier, e.g. a nonce, uuid, hash of signature, etc.)
2. Creates an empty rent-free PDA at that address
3. If the address exists, the whole transaction fails
4. Prepend or append this instruction to your transaction.
We also deployed a reference implementation to public networks so you can get started quickly:
| | |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **Cost per nullifier** | \~15,000 lamports (\~0.000015 SOL) |
| **Program ID** | `NFLx5WGPrTHHvdRNsidcrNcLxRruMC92E4yv7zhZBoT` |
| **Networks** | Mainnet, Devnet |
| **Source code** | [github.com/Lightprotocol/nullifier-program](https://github.com/Lightprotocol/nullifier-program/) |
| **Example Tx** | [Solana Explorer](https://explorer.solana.com/tx/38fA6kbKRcYb5XSez9ffQzfCcMbMHcaJGseCogShNXC5SemQsEo88ZMSPCLP9xv9PG8qSJnhWvWFqSYJnfBMLrpB) |
Example source code: [TypeScript](https://github.com/Lightprotocol/nullifier-program/tree/main/examples/action-create-nullifier.ts) | [Rust](https://github.com/Lightprotocol/nullifier-program/tree/main/examples/rust)
## Dependencies
```toml theme={null}
[dependencies]
light-nullifier-program = "0.1.2"
light-client = "0.23.0"
```
```bash theme={null}
npm install @lightprotocol/nullifier-program @lightprotocol/stateless.js@^0.23.0
```
## Using the Helper
```rust theme={null}
use light_nullifier_program::sdk::{create_nullifier_ix, PROGRAM_ID};
use light_client::{LightClient, LightClientConfig};
use solana_sdk::{system_instruction, transaction::Transaction};
let mut rpc = LightClient::new(
LightClientConfig::new("https://mainnet.helius-rpc.com/?api-key=...")
).await?;
// Create a unique 32-byte ID
let id: [u8; 32] = /* hash of payment inputs or random */;
// Build nullifier instruction
let nullifier_ix = create_nullifier_ix(&mut rpc, payer.pubkey(), id).await?;
// Combine with your transaction
let transfer_ix = system_instruction::transfer(&payer.pubkey(), &recipient, 1_000_000);
let tx = Transaction::new_signed_with_payer(
&[nullifier_ix, transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
```
```typescript theme={null}
import { createNullifierIx } from "@lightprotocol/nullifier-program";
import { createRpc } from "@lightprotocol/stateless.js";
import { Transaction, SystemProgram, ComputeBudgetProgram } from "@solana/web3.js";
const rpc = createRpc("https://mainnet.helius-rpc.com/?api-key=...");
// Create a unique 32-byte ID (e.g., hash of payment inputs)
const id = new Uint8Array(32);
crypto.getRandomValues(id);
// Build nullifier instruction
const nullifierIx = await createNullifierIx(rpc, payer.publicKey, id);
// Combine with your transaction
const tx = new Transaction().add(
nullifierIx,
// your main instruction
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: recipient,
lamports: 1_000_000,
})
);
```
## Manually Fetching Proof
```rust theme={null}
use light_nullifier_program::sdk::{fetch_proof, build_instruction};
// Step 1: Fetch proof
let proof_result = fetch_proof(&mut rpc, &id).await?;
// Step 2: Build instruction
let nullifier_ix = build_instruction(payer.pubkey(), id, proof_result);
// Add to transaction
let tx = Transaction::new_signed_with_payer(
&[nullifier_ix, transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
```
```typescript theme={null}
import { fetchProof, buildInstruction } from "@lightprotocol/nullifier-program";
// Step 1: Fetch proof
const proofResult = await fetchProof(rpc, id);
// Step 2: Build instruction
const nullifierIx = buildInstruction(payer.publicKey, id, proofResult);
// Combine with your transaction
const tx = new Transaction().add(
nullifierIx,
// your main instruction
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: recipient,
lamports: 1_000_000,
})
);
```
## Check If Nullifier Exists
```rust theme={null}
use light_nullifier_program::sdk::derive_nullifier_address;
let address = derive_nullifier_address(&id);
let account = rpc.get_compressed_account(None, Some(address)).await?;
let exists = account.value.is_some();
```
```typescript theme={null}
import { deriveNullifierAddress } from "@lightprotocol/nullifier-program";
import { bn } from "@lightprotocol/stateless.js";
const address = deriveNullifierAddress(id);
const account = await rpc.getCompressedAccount(bn(address.toBytes()));
const exists = account !== null;
```
***
Note that this is a reference implementation. Feel free to fork the program as you see fit.
Questions or need support? [Telegram](https://t.me/swen_light) | [email](mailto:support@lightprotocol.com) | [Discord](https://discord.com/invite/7cJ8BhAXhu)
```text theme={null}
---
description: Create rent-free nullifier PDAs to prevent duplicate actions
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Create rent-free nullifier PDAs to prevent duplicate actions
Context:
- Guide: https://zkcompression.com/compressed-pdas/nullifier-pda
- Skills and resources index: https://zkcompression.com/skill.md
- Dedicated skill: https://github.com/Lightprotocol/skills/tree/main/skills/zk-nullifier
- Rust crates: light-nullifier-program, light-client
- TS packages: @lightprotocol/nullifier-program, @lightprotocol/stateless.js
- Example: https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/actions/create_nullifier.rs
- Program source: https://github.com/Lightprotocol/nullifier-program/
Key APIs:
- Rust: create_nullifier_ix(), fetch_proof(), build_instruction(), derive_nullifier_address()
- TS: createNullifierIx(), fetchProof(), buildInstruction(), deriveNullifierAddress()
### 1. Index project
- Grep `nullifier|create_nullifier|createNullifierIx|deriveNullifierAddress|NFLx5WGPrTHHvdRNsidcrNcLxRruMC92E4yv7zhZBoT` across src/
- Glob `**/*.rs` and `**/*.ts` for project structure
- Identify: existing transaction building, duplicate prevention logic, payment flow
- Check Cargo.toml or package.json for existing light-* dependencies
- Task subagent (Grep/Read/WebFetch) if project has multiple packages to scan in parallel
### 2. Read references
- WebFetch the guide above — review both Rust and TS code samples
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: Rust or TypeScript?
- AskUserQuestion: what is the goal? (prevent duplicate payments, idempotent instruction execution, other use case)
- AskUserQuestion: do you need the helper (create_nullifier_ix) or manual proof fetching (fetch_proof + build_instruction)?
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan: which files to modify, what code to add, dependency changes
- Follow the guide's pattern: create unique 32-byte ID → build nullifier instruction → prepend to transaction
- If checking existence is needed, add derive_nullifier_address + get_compressed_account check
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- For Rust: Bash `cargo add light-nullifier-program@0.1 light-client@0.23`
- For TypeScript: Bash `npm install @lightprotocol/nullifier-program @lightprotocol/stateless.js@^0.23.0`
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- Rust: Bash `cargo check` + `cargo test` if tests exist
- TypeScript: Bash `tsc --noEmit` + run existing test suite if present
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work
```
# Overview & Program Template
Source: https://www.zkcompression.com/compressed-pdas/overview
Compressed PDAs provide full composability and functionality of accounts at PDAs, without rent-exemption cost per account. Suited for accounts where each user or entity gets their own PDA and state is infrequently accessed.
## What are Compressed PDAs
Compressed PDAs are compressed accounts with a unique address.
Programs invoke the Light System program to create and update compressed accounts, instead of the System program.
Fully compatible with existing Solana programs, but requires custom logic.
| Creation Cost | Regular PDA | Compressed PDA |
| :------------------- | -----------: | -------------: |
| **100-byte account** | \~0.0016 SOL | 0.000015 SOL |
## Use Cases
Compressed PDAs are suited for:
* Per-user state (profiles, preferences, credentials)
* DePIN node accounts and stake accounts
* Nullifiers to prevent an on-chain instruction from
being executed twice (little implementation overhead and useful e.g. for [payments](/compressed-pdas/nullifier-pda) or [ZK programs](zk/overview))
* App state that is written to and read from infrequently
## How it Works
# Start Building with the Program Template
### Prerequisites
Required versions:
* **Rust**: 1.90.0 or later
* **Solana CLI**: 2.3.11 or later
* **Anchor CLI**: 0.31.1
* **Zk compression CLI**: 0.27.0 or later
* **Node.js**: 22 or later
**Install Solana CLI:**
```bash theme={null}
sh -c "$(curl -sSfL https://release.solana.com/v2.2.15/install)"
```
**Install Anchor CLI:**
```bash theme={null}
cargo install --git https://github.com/coral-xyz/anchor avm --force
avm install latest
avm use latest
```
**Install the Light CLI:**
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
**Verify installation:**
```bash theme={null}
light --version
```
### Initialize your Program
Instantiate a template Solana program with compressed accounts with all required dependencies.
```bash theme={null}
light init testprogram
```
The `light init` command creates only Anchor-based projects . For Pinocchio programs, manually configure dependencies using `light-sdk-pinocchio`.
**Rust Crates**
* `light-sdk` - Core SDK for compressed accounts in native and anchor programs
* `light-sdk-pinocchio` Core SDK for compressed accounts in pinocchio programs
* `light-client` - RPC client and indexer for interacting with compressed accounts
* `light-program-test` - Testing utilities for compressed programs.
**TypeScript/JavaScript Packages**
* `@lightprotocol/stateless.js` - Client library for interacting with compressed accounts
* `@lightprotocol/zk-compression-cli` - Command-line tools for ZK compression development
### Build and Test
Now `cd testprogram` and run:
```bash theme={null}
anchor build
# Success: Finished `release` profile [optimized] target(s), after compiling.
# Note: Stack offset warnings are expected and don't prevent compilation
```
```bash theme={null}
cargo test-sbf
# Success: test result: ok. 1 passed; 0 failed; 0 ignored
```
## Common Errors
```shellscript theme={null}
Fix:
In your terminal, run:
1. export CC=$(xcrun -find clang)
2. export SDKROOT=$(xcrun --show-sdk-path)
3. cargo clean
4. anchor build
Example log:
The following warnings were emitted during compilation:
warning: blake3@1.5.1: In file included from c/blake3_neon.c:1:
warning: blake3@1.5.1: c/blake3_impl.h:4:10: fatal error: 'assert.h' file not found
warning: blake3@1.5.1: 4 | #include
warning: blake3@1.5.1: | ^~~~~~~~~~
warning: blake3@1.5.1: 1 error generated.
error: failed to run custom build command for `blake3 v1.5.1`
Caused by:
process didn't exit successfully: `/Users/you/testprogram/target/release/build/blake3-ac41d29c2eabe052/build-script-build` (exit status: 1)
--- stdout
cargo:rerun-if-env-changed=CARGO_FEATURE_PURE
cargo:rerun-if-env-changed=CARGO_FEATURE_NO_NEON
cargo:rerun-if-env-changed=CARGO_FEATURE_NEON
cargo:rerun-if-env-changed=CARGO_FEATURE_NEON
cargo:rerun-if-env-changed=CARGO_FEATURE_NO_NEON
cargo:rerun-if-env-changed=CARGO_FEATURE_PURE
cargo:rustc-cfg=blake3_neon
OUT_DIR = Some(/Users/you/testprogram/target/release/build/blake3-735a4c71d985df30/out)
TARGET = Some(aarch64-apple-darwin)
OPT_LEVEL = Some(3)
HOST = Some(aarch64-apple-darwin)
cargo:rerun-if-env-changed=CC_aarch64-apple-darwin
CC_aarch64-apple-darwin = None
cargo:rerun-if-env-changed=CC_aarch64_apple_darwin
CC_aarch64_apple_darwin = None
cargo:rerun-if-env-changed=HOST_CC
HOST_CC = None
cargo:rerun-if-env-changed=CC
CC = Some(/Users/you/.local/share/solana/install/releases/1.18.22/solana-release/bin/sdk/sbf/dependencies/platform-tools/llvm/bin/clang)
RUSTC_WRAPPER = None
cargo:rerun-if-env-changed=CC_ENABLE_DEBUG_OUTPUT
cargo:rerun-if-env-changed=CRATE_CC_NO_DEFAULTS
CRATE_CC_NO_DEFAULTS = None
DEBUG = Some(false)
cargo:rerun-if-env-changed=MACOSX_DEPLOYMENT_TARGET
MACOSX_DEPLOYMENT_TARGET = None
cargo:rerun-if-env-changed=CFLAGS_aarch64-apple-darwin
CFLAGS_aarch64-apple-darwin = None
cargo:rerun-if-env-changed=CFLAGS_aarch64_apple_darwin
CFLAGS_aarch64_apple_darwin = None
cargo:rerun-if-env-changed=HOST_CFLAGS
HOST_CFLAGS = None
cargo:rerun-if-env-changed=CFLAGS
CFLAGS = None
cargo:warning=In file included from c/blake3_neon.c:1:
cargo:warning=c/blake3_impl.h:4:10: fatal error: 'assert.h' file not found
cargo:warning= 4 | #include
cargo:warning= | ^~~~~~~~~~
cargo:warning=1 error generated.
--- stderr
error occurred: Command env -u IPHONEOS_DEPLOYMENT_TARGET "/Users/you/.local/share/solana/install/releases/1.18.22/solana-release/bin/sdk/sbf/dependencies/platform-tools/llvm/bin/clang" "-O3" "-ffunction-sections" "-fdata-sections" "-fPIC" "--target=arm64-apple-darwin" "-mmacosx-version-min=14.4" "-Wall" "-Wextra" "-std=c11" "-o" "/Users/you/testprogram/target/release/build/blake3-735a4c71d985df30/out/db3b6bfb95261072-blake3_neon.o" "-c" "c/blake3_neon.c" with args clang did not execute successfully (status code exit status: 1).
```
## Guides
|
|
|
Create
|
Initialize compressed PDAs in your program |
|
Update
|
Modify state in compressed accounts |
|
Close
|
Reclaim lamports from compressed accounts |
|
Reinitialize
|
Reset and reuse compressed accounts |
|
Burn
|
Permanently delete compressed accounts |
|
Nullifier PDAs
|
Prevent replay attacks with one-time use accounts |
## Program Examples
| Example | Description |
| :--------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Account Comparison](https://github.com/Lightprotocol/program-examples/tree/main/account-comparison) | Compare compressed accounts with standard Solana accounts |
| [basic-operations/anchor](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor) | Anchor programs to create, update, close, reinitialize and burn compressed accounts with Rust and TypeScript tests |
| [basic-operations/native](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native) | Native Solana program implementation to create, update, close, reinitialize and burn compressed accounts with Rust tests |
| [Counter (Anchor)](https://github.com/Lightprotocol/program-examples/tree/main/counter/anchor) | Full compressed account lifecycle (create, increment, decrement, reset, close) using Anchor framework |
| [Counter (Native)](https://github.com/Lightprotocol/program-examples/tree/main/counter/native) | Native Solana program implementation with Rust tests |
| [Counter (Pinocchio)](https://github.com/Lightprotocol/program-examples/tree/main/counter/pinocchio) | Pinocchio implementation using light-sdk-pinocchio with Rust tests |
| [Create-and-Update](https://github.com/Lightprotocol/program-examples/tree/main/create-and-update) | Create new compressed accounts and update existing ones within a single instruction and one validity proof |
| [merkle-distributor](https://github.com/Lightprotocol/distributor) | SPL token distribution with compressed PDAs for claim tracking, vesting, and clawback |
| [Nullifier Program](https://github.com/Lightprotocol/nullifier-program) | Primitive to enforce that a given onchain instruction (such as a payment) executes only once, and to track execution by unique identifiers (e.g. a nonce, uuid, etc.) |
| [Read-Only](https://github.com/Lightprotocol/program-examples/tree/main/read-only) | Create compressed accounts and read them on-chain |
| [simple-claim](https://github.com/Lightprotocol/program-examples-airdrop-implementations/tree/main/simple-claim) | Distributes compressed tokens that decompress to SPL on claim |
| [ZK-ID](https://github.com/Lightprotocol/program-examples/tree/main/zk-id) | Zero-knowledge proofs for identity verification with compressed accounts |
| [ZK-Nullifier](https://github.com/Lightprotocol/program-examples/tree/main/zk-nullifier) | Implementation of nullifiers for zk programs on Solana to prevent double spending |
## SDK Reference
### Client
TypeScript RPC client for compressed accounts, validity proofs, and address derivation.
Rust RPC client and indexer for compressed accounts.
### Program
Core SDK for compressed accounts in Anchor programs.
Procedural macros for LightAccount derivation.
Local testing framework for programs.
# Next Steps
# Program Examples
Source: https://www.zkcompression.com/compressed-pdas/program-examples
Program example repository for compressed accounts with tests.
## Airdrop claim reference implementations
* **Basic**: [**simple-claim**](https://github.com/Lightprotocol/program-examples/tree/main/airdrop-implementations/simple-claim) - Distributes compressed tokens that get decompressed to SPL on claim with cliff.
* **Advanced**: [**merkle-distributor**](https://github.com/Lightprotocol/program-examples/tree/main/airdrop-implementations/distributor) - Distributes SPL tokens, uses compressed PDAs to track claims with linear vesting, partial claims and clawback. Based on Jito Merkle distributor and optimized with rent-free PDAs.
For simple client-side distribution visit [this example](https://github.com/Lightprotocol/example-token-distribution).
## Basic operations
* **create** - Initialize a new compressed account
* [Anchor](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/create) | [Native](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/create)
* **update** - Modify data in an existing compressed account
* [Anchor](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/update) | [Native](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/update)
* **close** - Clear account data and preserve its address
* [Anchor](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/close) | [Native](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/close)
* **reinit** - Reinitialize a closed account with the same address
* [Anchor](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/reinit) | [Native](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/reinit)
* **burn** - Permanently delete a compressed account
* [Anchor](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/anchor/burn) | [Native](https://github.com/Lightprotocol/program-examples/tree/main/basic-operations/native/programs/burn)
## Nullifier program
[**nullifier-program**](https://github.com/Lightprotocol/nullifier-program) - For some use cases, such as sending payments, you might want to prevent your onchain instruction from being executed more than once. Creates a rent-free PDA derived from an id. If the id has been used before, the PDA already exists, causing the instruction to fail.
SDK: [`light-nullifier-program`](https://docs.rs/light-nullifier-program) | [Example client usage](https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/actions/create_nullifier.rs)
## Counter program
Full compressed account lifecycle (create, increment, decrement, reset, close):
* [**counter/anchor**](https://github.com/Lightprotocol/program-examples/tree/main/counter/anchor) - Anchor program with Rust and TypeScript tests
* [**counter/native**](https://github.com/Lightprotocol/program-examples/tree/main/counter/native) - Native Solana program with `light-sdk` and Rust tests
* [**counter/pinocchio**](https://github.com/Lightprotocol/program-examples/tree/main/counter/pinocchio) - Pinocchio program with `light-sdk-pinocchio` and Rust tests
## Create-and-update program
[**create-and-update**](https://github.com/Lightprotocol/program-examples/tree/main/create-and-update) - Create a new compressed account and update an existing compressed account with a single validity proof in one instruction.
## Create-and-read program
[**read-only**](https://github.com/Lightprotocol/program-examples/tree/main/read-only) - Create a new compressed account and read it onchain.
## Compare program with Solana vs compressed accounts
[**account-comparison**](https://github.com/Lightprotocol/program-examples/tree/main/account-comparison) - Compare compressed vs regular Solana accounts.
## ZK programs
* [**zk-id**](https://github.com/Lightprotocol/program-examples/tree/main/zk/zk-id) - Identity verification using Groth16 proofs. Issuers create credentials; users prove ownership without revealing the credential.
* [**nullifier**](https://github.com/Lightprotocol/program-examples/tree/main/zk/nullifier) - Simple program to create nullifiers for ZK programs.
## Next steps
# Solana Attestation Service (Devnet)
Source: https://www.zkcompression.com/compressed-pdas/solana-attestation-service
Credential system for KYC verification, professional badges, and compliance credentials in minutes. Currently undergoing audit. On mainnet today, you can integrate compressed PDAs to achieve similar functionality.
View the attestation examples: [Solana Attestation Service](https://github.com/solana-foundation/solana-attestation-service/tree/a8fb187978ecea0b165e63767c7e6b1c036e7b52/examples/typescript/attestation-flow-guides#readme).
# Combine Instructions in One Transaction
Source: https://www.zkcompression.com/compressed-tokens/combine-instructions
Guide to combine multiple instructions in a single transaction. Full code example for interface PDA creation and for first-time compression of existing SPL tokens.
The SDK provides instruction-level APIs that return instructions without sending transactions. Combine these instructions to build custom transactions with multiple instructions.
This guide demonstrates creating an interface PDA and compressing existing SPL tokens in a single transaction.
# Full Code Example
### Prerequisites
Make sure you have dependencies and developer environment set up!
**Dependencies**
```bash theme={null}
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
```bash theme={null}
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
```bash theme={null}
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
```bash theme={null}
# npm
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# yarn
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# pnpm
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
```
**Developer Environment**
By default, all guides use Localnet.
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
# Start a local test validator
light test-validator
## ensure you have the Solana CLI accessible in your system PATH
```
```typescript theme={null}
// createRpc() defaults to local test validator endpoints
import {
Rpc,
createRpc,
} from "@lightprotocol/stateless.js";
const connection: Rpc = createRpc();
async function main() {
let slot = await connection.getSlot();
console.log(slot);
let health = await connection.getIndexerHealth(slot);
console.log(health);
// "Ok"
}
main();
```
Replace `` with your actual API key. [Get your API key here](https://www.helius.dev/zk-compression), if you don't have one yet.
```typescript theme={null}
import { createRpc } from "@lightprotocol/stateless.js";
const RPC_ENDPOINT = "https://devnet.helius-rpc.com?api-key=";
const connection = createRpc(RPC_ENDPOINT);
async function main() {
let slot = await connection.getSlot();
console.log(slot);
let health = await connection.getIndexerHealth(slot);
console.log(health);
// "Ok"
}
main();
```
### Create Interface PDA + Compress
```typescript create-pool-and-compress.ts highlight={26,71} expandable theme={null}
// 1. Setup: Create regular SPL token and mint to ATA
// 2. Build instructions for create interface PDA and compress
// 3. Combine instructions in one transaction
// 4. Verify compressed balance
import {
Keypair,
ComputeBudgetProgram,
} from '@solana/web3.js';
import {
createRpc,
buildAndSignTx,
sendAndConfirmTx,
selectStateTreeInfo,
} from '@lightprotocol/stateless.js';
import { CompressedTokenProgram } from '@lightprotocol/compressed-token';
import {
createMint,
getOrCreateAssociatedTokenAccount,
mintTo,
TOKEN_PROGRAM_ID,
} from '@solana/spl-token';
import BN from 'bn.js';
async function createPoolAndCompress() {
// Step 1: Setup - Create regular SPL token and mint to ATA
const rpc = createRpc();
const payer = Keypair.generate();
const airdropSignature = await rpc.requestAirdrop(payer.publicKey, 1000000000);
await rpc.confirmTransaction(airdropSignature);
// Create regular SPL token mint
const mint = await createMint(
rpc,
payer,
payer.publicKey,
null,
9,
undefined,
undefined,
TOKEN_PROGRAM_ID
);
// Create ATA and mint tokens to it
const ata = await getOrCreateAssociatedTokenAccount(
rpc,
payer,
mint,
payer.publicKey,
undefined,
undefined,
undefined,
TOKEN_PROGRAM_ID
);
await mintTo(
rpc,
payer,
mint,
ata.address,
payer,
1_000_000_000, // 1 token with 9 decimals
undefined,
undefined,
TOKEN_PROGRAM_ID
);
console.log("Regular SPL token created:", mint.toBase58());
console.log("ATA balance:", 1, "token");
// Step 2: Build instructions for create interface PDA and compress
const outputStateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos());
// Derive interface PDA
const tokenPoolPda = CompressedTokenProgram.deriveTokenPoolPda(mint);
// Create interface PDA instruction
const createTokenPoolIx = await CompressedTokenProgram.createTokenPool({
feePayer: payer.publicKey,
mint,
tokenProgram: TOKEN_PROGRAM_ID,
});
// Manually construct TokenPoolInfo for first-time compression
const tokenPoolInfo = {
mint: mint,
tokenPoolPda: tokenPoolPda,
tokenProgram: TOKEN_PROGRAM_ID,
isInitialized: true, // Set to true even though pool will be created in this tx
balance: new BN(0),
poolIndex: 0,
bump: 0, // Placeholder value
};
// Create compress instruction
const compressIx = await CompressedTokenProgram.compress({
outputStateTreeInfo,
tokenPoolInfo,
payer: payer.publicKey,
owner: payer.publicKey,
source: ata.address,
toAddress: payer.publicKey,
amount: new BN(1_000_000_000),
mint,
});
// Step 3: Combine instructions in one transaction
const { blockhash } = await rpc.getLatestBlockhash();
const allInstructions = [
ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }),
createTokenPoolIx,
compressIx,
];
const tx = buildAndSignTx(
allInstructions,
payer,
blockhash,
[]
);
const txId = await sendAndConfirmTx(rpc, tx);
console.log("Interface PDA created and tokens compressed");
console.log("Transaction:", txId);
// Step 4: Verify compressed balance
const compressedAccounts = await rpc.getCompressedTokenAccountsByOwner(
payer.publicKey,
{ mint }
);
const compressedBalance = compressedAccounts.items.reduce(
(sum, account) => sum.add(account.parsed.amount),
new BN(0)
);
console.log("Compressed balance:", compressedBalance.toNumber() / 1_000_000_000, "tokens");
return {
transactionSignature: txId,
mint,
compressedBalance: compressedBalance.toNumber(),
};
}
createPoolAndCompress().catch(console.error);
```
# Next Steps
# Create Interface PDAs for Compression to Existing Mints
Source: https://www.zkcompression.com/compressed-tokens/guides/add-token-pools-to-mint-accounts
Create an interface PDA for an existing SPL mint. Requires only fee_payer with no mint authority constraint.
Create an interface PDA for compression for an existing SPL mint. `createTokenPool()` requires only `fee_payer` and has no mint authority constraint.
The interface PDA itself requires rent, but individual compressed token accounts are rent-free.
```typescript function-create-token-pool.ts theme={null}
// Creates interface PDA for existing SPL mint
const transactionSignature = await createTokenPool(
rpc,
payer,
mint,
);
```
**Best Practice:** Each mint supports a maximum of 4 interface PDAs total. During compression/decompression operations, interface PDAs get write-locked. Use `addTokenPools()` to create additional PDAs that increase per-block write-lock capacity.
## Get Started
### Create Interface PDA
Install packages in your working directory:
```bash theme={null}
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
# npm
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# yarn
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# pnpm
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
```
Install the CLI globally:
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
# start local test-validator in a separate terminal
light test-validator
```
In the code examples, use `createRpc()` without arguments for localnet.
Get an API key from [Helius](https://helius.dev) and add to `.env`:
```bash title=".env" theme={null}
API_KEY=
```
In the code examples, use `createRpc(RPC_URL)` with the devnet URL.
```typescript theme={null}
import "dotenv/config";
import { Keypair, PublicKey } from "@solana/web3.js";
import { createRpc } from "@lightprotocol/stateless.js";
import { createTokenPool } from "@lightprotocol/compressed-token";
import { createMint as createSplMint, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { homedir } from "os";
import { readFileSync } from "fs";
// devnet:
const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
// localnet:
// const RPC_URL = undefined;
const payer = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
)
);
(async function () {
// devnet:
const rpc = createRpc(RPC_URL);
// localnet:
// const rpc = createRpc();
// Setup: Create existing SPL mint
const mintKeypair = Keypair.generate();
await createSplMint(rpc, payer, payer.publicKey, null, 9, mintKeypair, undefined, TOKEN_PROGRAM_ID);
// Create interface PDA for existing mint
const tx = await createTokenPool(rpc, payer, mintKeypair.publicKey);
console.log("Mint:", mintKeypair.publicKey.toBase58());
console.log("Tx:", tx);
})();
```
# Troubleshooting
You're trying to access an interface PDA that doesn't exist.
```typescript theme={null}
// Create the missing interface PDA
const poolTx = await createTokenPool(rpc, payer, mint);
console.log("Interface PDA created:", poolTx);
```
# Advanced Configuration
Create pools for multiple mints:
```typescript theme={null}
const mints = [
new PublicKey("MINT_1_ADDRESS"),
new PublicKey("MINT_2_ADDRESS"),
new PublicKey("MINT_3_ADDRESS"),
];
for (const mint of mints) {
try {
const poolTx = await createTokenPool(rpc, payer, mint);
console.log(`Pool created for ${mint.toBase58()}:`, poolTx);
} catch (error) {
console.log(`Failed for ${mint.toBase58()}:`, error.message);
}
}
```
Create interface PDAs for Token-2022 mints:
```typescript theme={null}
import { TOKEN_2022_PROGRAM_ID } from '@solana/spl-token';
const poolTx = await createTokenPool(
rpc,
payer,
mint, // Token-2022 mint
undefined,
TOKEN_2022_PROGRAM_ID,
);
```
# Next Steps
# Compress and Decompress SPL Tokens
Source: https://www.zkcompression.com/compressed-tokens/guides/compress-decompress
Convert SPL tokens between compressed and regular format with compress() and decompress().
```typescript compress() theme={null}
// Compress SPL tokens to compressed tokens
const compressionSignature = await compress(
rpc,
payer,
mint, // SPL mint with interface PDA for compression
amount,
payer, // owner of SPL tokens
tokenAccount.address, // source SPL token account (sourceTokenAccount parameter)
recipient, // recipient owner address (toAddress parameter)
);
```
```typescript decompress() theme={null}
// Decompress compressed tokens to SPL tokens
const transactionSignature = await decompress(
rpc,
payer,
mint, // SPL mint with interface PDA for compression
amount,
payer, // owner of compressed tokens
tokenAccount.address, // destination token account (toAddress parameter)
);
```
**Function Difference and Best Practice:**
* `compress(amount, sourceTokenAccount, toAddress)` compresses specific amounts from source to a specified recipient. Use for transfers and precise amounts.
* `compressSplTokenAccount(tokenAccount, remainingAmount)` compresses the entire SPL token account balance minus optional remaining amount only to the same owner. Use to migrate complete token accounts with optional partial retention. [Here is how](/compressed-tokens/guides/compress-spl-token-account).
## Get Started
### Compress / Decompress Tokens
Install packages in your working directory:
```bash theme={null}
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
# npm
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# yarn
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# pnpm
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
```
Install the CLI globally:
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
# start local test-validator in a separate terminal
light test-validator
```
In the code examples, use `createRpc()` without arguments for localnet.
Get an API key from [Helius](https://helius.dev) and add to `.env`:
```bash title=".env" theme={null}
API_KEY=
```
In the code examples, use `createRpc(RPC_URL)` with the devnet URL.
Before we can compress or decompresss, we need:
* An SPL mint with a token pool for compression. This token pool can be created for new SPL mints via [`createMint()`](/compressed-tokens/guides/create-mint-with-token-pool) or added to existing SPL mints via [`createTokenPool()`](/compressed-tokens/guides/add-token-pools-to-mint-accounts).
* For `compress()` SPL tokens in an Associated Token Account, or
* For `decompress()` compressed token accounts with sufficient balance.
```typescript theme={null}
import "dotenv/config";
import { Keypair } from "@solana/web3.js";
import { createRpc } from "@lightprotocol/stateless.js";
import { createMint, mintTo, decompress, compress } from "@lightprotocol/compressed-token";
import { createAssociatedTokenAccount } from "@solana/spl-token";
import { homedir } from "os";
import { readFileSync } from "fs";
// devnet:
const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
// localnet:
// const RPC_URL = undefined;
const payer = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
)
);
(async function () {
// devnet:
const rpc = createRpc(RPC_URL);
// localnet:
// const rpc = createRpc();
// Setup: Get SPL tokens (needed to compress)
const { mint } = await createMint(rpc, payer, payer.publicKey, 9);
const splAta = await createAssociatedTokenAccount(rpc, payer, mint, payer.publicKey);
await mintTo(rpc, payer, mint, payer.publicKey, payer, 1_000_000_000);
await decompress(rpc, payer, mint, 1_000_000_000, payer, splAta);
// Compress SPL tokens
const recipient = Keypair.generate();
const tx = await compress(rpc, payer, mint, 500_000_000, payer, splAta, recipient.publicKey);
console.log("Mint:", mint.toBase58());
console.log("Tx:", tx);
})();
```
```typescript theme={null}
import "dotenv/config";
import { Keypair } from "@solana/web3.js";
import { createRpc } from "@lightprotocol/stateless.js";
import { createMint, mintTo, decompress } from "@lightprotocol/compressed-token";
import { createAssociatedTokenAccount } from "@solana/spl-token";
import { homedir } from "os";
import { readFileSync } from "fs";
// devnet:
const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
// localnet:
// const RPC_URL = undefined;
const payer = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
)
);
(async function () {
// devnet:
const rpc = createRpc(RPC_URL);
// localnet:
// const rpc = createRpc();
// Setup: Create mint and mint compressed tokens
const { mint } = await createMint(rpc, payer, payer.publicKey, 9);
await mintTo(rpc, payer, mint, payer.publicKey, payer, 1_000_000_000);
const splAta = await createAssociatedTokenAccount(rpc, payer, mint, payer.publicKey);
// Decompress to SPL tokens
const tx = await decompress(rpc, payer, mint, 500_000_000, payer, splAta);
console.log("Mint:", mint.toBase58());
console.log("Tx:", tx);
})();
```
The SPL mint must have an SPL Interface PDA for compression.
The script creates it for you.
For development, create a new mint with SPL interface via [`createMint()`](https://github.com/Lightprotocol/examples-zk-compression/tree/main/compressed-token-cookbook) or add an SPL interface to an existing mint via [`createSplInterface()`](https://github.com/Lightprotocol/examples-zk-compression/tree/main/compressed-token-cookbook).
# Troubleshooting
Check your balances before operations:
```typescript theme={null}
// For decompression - check compressed balance
const compressedAccounts = await rpc.getCompressedTokenAccountsByOwner(
owner.publicKey,
{ mint }
);
const compressedBalance = compressedAccounts.items.reduce(
(sum, account) => sum.add(account.parsed.amount),
new BN(0)
);
// For compression - check SPL token balance
const tokenBalance = await rpc.getTokenAccountBalance(tokenAccount);
const splBalance = new BN(tokenBalance.value.amount);
console.log("Can decompress up to:", compressedBalance.toString());
console.log("Can compress up to:", splBalance.toString());
```
Ensure the signer owns the tokens being decompressed/compressed:
```typescript theme={null}
// The owner parameter must be the actual owner
const decompressTx = await decompress(
rpc,
payer, // can be different (pays fees)
mint,
amount,
actualOwner, // must own compressed tokens
destinationAta,
);
const compressTx = await compress(
rpc,
payer, // can be different (pays fees)
mint,
amount,
actualOwner, // must own SPL tokens
sourceAta,
recipient,
);
```
# Advanced Configuration
Compress tokens directly to someone else:
```typescript theme={null}
const recipientWallet = new PublicKey("RECIPIENT_WALLET_ADDRESS");
// Compress your SPL tokens to recipient
const compressTx = await compress(
rpc,
payer,
mint,
amount,
tokenOwner, // current owner signs
tokenAccount, // your token account
recipientWallet, // recipient gets compressed tokens
);
```
Compress multiple token accounts:
```typescript theme={null}
// Compress to multiple recipients at once
const recipients = [recipient1.publicKey, recipient2.publicKey, recipient3.publicKey];
const amounts = [1_000_000_000, 2_000_000_000, 500_000_000]; // Different amounts
const batchCompressTx = await compress(
rpc,
payer,
mint,
amounts, // Array of amounts
owner,
tokenAccount,
recipients, // Array of recipients
);
console.log("Batch compression completed:", batchCompressTx);
```
Decompress tokens using delegate authority:
```typescript theme={null}
import { decompressDelegated } from '@lightprotocol/compressed-token';
import { getAssociatedTokenAddress, TOKEN_PROGRAM_ID } from '@solana/spl-token';
// Get ATA for decompressed tokens
const ataAddress = await getAssociatedTokenAddress(
mint,
recipient,
false,
TOKEN_PROGRAM_ID
);
// Delegate decompresses tokens
await decompressDelegated(
rpc,
payer,
mint,
amount,
delegate, // Signer - owner of compressed tokens
ataAddress, // Uncompressed token account (ATA)
);
```
# Next Steps
# Compress Complete SPL Token Accounts
Source: https://www.zkcompression.com/compressed-tokens/guides/compress-spl-token-account
Compress the entire balance of an SPL token account to reclaim rent. Optionally leave some tokens in SPL format.
```typescript compressSplTokenAccount() theme={null}
// Compress entire SPL token account balance
const transactionSignature = await compressSplTokenAccount(
rpc,
payer,
mint, // SPL mint with interface PDA for compression
owner,
tokenAccount, // SPL token account to compress
);
```
```typescript partialCompression() theme={null}
// Compress account while keeping some tokens in SPL format
const transactionSignature = await compressSplTokenAccount(
rpc,
payer,
mint, // SPL mint with interface PDA for compression
owner,
tokenAccount, // SPL token account to compress
remainingAmount, // amount to keep in SPL format
);
```
After compression, empty token accounts can now be closed to reclaim rent with [`closeAccount()`](https://solana.com/developers/cookbook/tokens/close-token-accounts).
**Function Difference and Best Practice:**
* `compressSplTokenAccount(tokenAccount, remainingAmount)` compresses the entire SPL token account balance minus optional remaining amount only to the same owner. Use to migrate complete token accounts with optional partial retention.
* `compress(amount, sourceTokenAccount, toAddress)` compresses specific amounts from source to a specified recipient. Use for transfers and precise amounts. [Here is how](/compressed-tokens/guides/compress-decompress).
## Get Started
### Compress SPL Token Accounts
Install packages in your working directory:
```bash theme={null}
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
# npm
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# yarn
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# pnpm
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
```
Install the CLI globally:
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
# start local test-validator in a separate terminal
light test-validator
```
In the code examples, use `createRpc()` without arguments for localnet.
Get an API key from [Helius](https://helius.dev) and add to `.env`:
```bash title=".env" theme={null}
API_KEY=
```
In the code examples, use `createRpc(RPC_URL)` with the devnet URL.
```typescript theme={null}
import "dotenv/config";
import { Keypair } from "@solana/web3.js";
import { createRpc, bn } from "@lightprotocol/stateless.js";
import { createMint, compressSplTokenAccount } from "@lightprotocol/compressed-token";
import { createAssociatedTokenAccount, mintTo, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { homedir } from "os";
import { readFileSync } from "fs";
// devnet:
const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
// localnet:
// const RPC_URL = undefined;
const payer = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
)
);
(async function () {
// devnet:
const rpc = createRpc(RPC_URL);
// localnet:
// const rpc = createRpc();
// Setup: Create mint and SPL token account with tokens
const { mint } = await createMint(rpc, payer, payer.publicKey, 9);
const owner = Keypair.generate();
const tokenAccount = await createAssociatedTokenAccount(
rpc,
payer,
mint,
owner.publicKey,
undefined,
TOKEN_PROGRAM_ID
);
await mintTo(rpc, payer, mint, tokenAccount, payer, bn(1_000_000_000).toNumber());
// Compress entire SPL token account balance
const tx = await compressSplTokenAccount(
rpc,
payer,
mint,
owner,
tokenAccount
);
console.log("Mint:", mint.toBase58());
console.log("Tx:", tx);
})();
```
The SPL mint must have an SPL Interface PDA for compression.
The script creates it for you.
For development, create a new mint with SPL interface via [`createMint()`](https://github.com/Lightprotocol/examples-zk-compression/tree/main/compressed-token-cookbook) or add an SPL interface to an existing mint via [`createSplInterface()`](https://github.com/Lightprotocol/examples-zk-compression/tree/main/compressed-token-cookbook).
# Troubleshooting
The token account doesn't have enough tokens for the operation.
```typescript theme={null}
// Check token account balance before compression
const balance = await rpc.getTokenAccountBalance(tokenAccount);
if (Number(balance.value.amount) === 0) {
console.log("Token account is empty");
return;
}
console.log("Available balance:", Number(balance.value.amount));
// Proceed with compression
const compressTx = await compressSplTokenAccount(
rpc,
payer,
mint,
owner,
tokenAccount,
);
```
The `remainingAmount` parameter exceeds the current account balance.
```typescript theme={null}
const balance = await rpc.getTokenAccountBalance(tokenAccount);
const availableAmount = Number(balance.value.amount);
const remainingAmount = bn(500_000_000); // 0.5 tokens
if (remainingAmount.gt(bn(availableAmount))) {
console.log(`Cannot leave ${remainingAmount.toString()} tokens`);
console.log(`Available balance: ${availableAmount}`);
throw new Error("Remaining amount exceeds balance");
}
// Use valid remaining amount
const compressTx = await compressSplTokenAccount(
rpc,
payer,
mint,
owner,
tokenAccount,
remainingAmount, // must be <= balance
);
```
# Advanced Configuration
Compress most tokens while leaving some in SPL format:
```typescript theme={null}
import { bn } from '@lightprotocol/stateless.js';
// Leave 100 tokens (0.1 with 9 decimals) in SPL account
const remainingAmount = bn(100_000_000);
const compressTx = await compressSplTokenAccount(
rpc,
payer,
mint,
owner,
tokenAccount,
remainingAmount, // amount to keep in SPL format
);
// Account will retain remainingAmount tokens
```
Compress several token accounts for the same mint:
```typescript theme={null}
const tokenAccounts = [
{ account: new PublicKey("ACCOUNT_1"), owner: owner1 },
{ account: new PublicKey("ACCOUNT_2"), owner: owner2 },
{ account: new PublicKey("ACCOUNT_3"), owner: owner3 },
];
// Compress each account
for (const { account, owner } of tokenAccounts) {
console.log(`Compressing account: ${account.toBase58()}`);
try {
const compressTx = await compressSplTokenAccount(
rpc,
payer,
mint,
owner,
account,
);
console.log(`Compressed: ${compressTx}`);
} catch (error) {
console.log(`Failed: ${error.message}`);
}
}
```
# Next Steps
# Create Compressed Token Accounts
Source: https://www.zkcompression.com/compressed-tokens/guides/create-compressed-token-accounts
Short guide to compressed token account creation with ZK Compression on Solana and difference to regular token accounts.
Compressed token accounts store ownership information for compressed tokens like regular token accounts with two core differences. Compressed Tokens
* do not require an Associated Token Accounts (ATAs), and
* do not require a rent-exempt balance.
Compressed token accounts are created in the following scenarios:
1. `mintTo()` creates compressed token accounts for recipients.
How to Mint Compressed Tokens
2. `transfer()` consumes existing accounts of the sender as input, and creates new compressed token accounts with updated balances as output for the sender and recipient(s).
Transfer Compressed Tokens
# Create a Mint Account with Interface PDA for Compression
Source: https://www.zkcompression.com/compressed-tokens/guides/create-mint-with-token-pool
Create an SPL token mint with an interface PDA for compression. The interface PDA locks tokens while compressed and releases them when decompressed.
The mint account itself requires rent (like regular SPL mints), but individual compressed token accounts are rent-free.
Create an interface PDA for an existing SPL mint with [`createTokenPool()`](/compressed-tokens/guides/add-token-pools-to-mint-accounts) or use `createMint()` to create a new one from scratch.
```typescript createMint() theme={null}
// Create SPL mint with interface PDA for compression
const { mint, transactionSignature } = await createMint(
rpc,
payer,
mintAuthority.publicKey,
decimals,
);
```
**Best Practice:** Each mint supports a maximum of 4 interface PDAs total. During compression/decompression, interface PDAs get write-locked. Use `addTokenPools()` to create additional PDAs that increase per-block write-lock capacity.
## Get Started
### Create a Mint Account with Interface PDA for Compression
Install packages in your working directory:
```bash theme={null}
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
# npm
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# yarn
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# pnpm
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
```
Install the CLI globally:
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
# start local test-validator in a separate terminal
light test-validator
```
In the code examples, use `createRpc()` without arguments for localnet.
Get an API key from [Helius](https://helius.dev) and add to `.env`:
```bash title=".env" theme={null}
API_KEY=
```
In the code examples, use `createRpc(RPC_URL)` with the devnet URL.
```typescript theme={null}
import "dotenv/config";
import { Keypair } from "@solana/web3.js";
import { createRpc } from "@lightprotocol/stateless.js";
import { createMint } from "@lightprotocol/compressed-token";
import { homedir } from "os";
import { readFileSync } from "fs";
// devnet:
const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
// localnet:
// const RPC_URL = undefined;
const payer = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
)
);
(async function () {
// devnet:
const rpc = createRpc(RPC_URL);
// localnet:
// const rpc = createRpc();
const { mint, transactionSignature } = await createMint(
rpc,
payer,
payer.publicKey,
9
);
console.log("Mint:", mint.toBase58());
console.log("Tx:", transactionSignature);
})();
```
```typescript theme={null}
import "dotenv/config";
import {
Keypair,
ComputeBudgetProgram,
PublicKey,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
createRpc,
getBatchAddressTreeInfo,
selectStateTreeInfo,
LIGHT_TOKEN_PROGRAM_ID,
} from "@lightprotocol/stateless.js";
import { createMintInstruction } from "@lightprotocol/compressed-token";
import { homedir } from "os";
import { readFileSync } from "fs";
const COMPRESSED_MINT_SEED = Buffer.from("compressed_mint");
function findMintAddress(mintSigner: PublicKey): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[COMPRESSED_MINT_SEED, mintSigner.toBuffer()],
LIGHT_TOKEN_PROGRAM_ID
);
}
// devnet:
const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
const rpc = createRpc(RPC_URL);
// localnet:
// const rpc = createRpc();
const payer = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
)
);
(async function () {
const mintSigner = Keypair.generate();
const addressTreeInfo = getBatchAddressTreeInfo();
const stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos());
const [mintPda] = findMintAddress(mintSigner.publicKey);
const validityProof = await rpc.getValidityProofV2(
[],
[{ address: mintPda.toBytes(), treeInfo: addressTreeInfo }]
);
const ix = createMintInstruction(
mintSigner.publicKey,
9,
payer.publicKey,
null,
payer.publicKey,
validityProof,
addressTreeInfo,
stateTreeInfo
);
const tx = new Transaction().add(
ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }),
ix
);
const signature = await sendAndConfirmTransaction(rpc, tx, [payer, mintSigner]);
console.log("Mint:", mintPda.toBase58());
console.log("Tx:", signature);
})();
```
# Advanced Configuration
Customize who can mint new compressed tokens.
```typescript theme={null}
const mintAuthority = Keypair.generate();
const { mint, transactionSignature } = await createMint(
rpc,
payer,
mintAuthority.publicKey,
9,
);
```
Customize who can freeze/thaw compressed token accounts.
```typescript theme={null}
const freezeAuthority = Keypair.generate();
const { mint, transactionSignature } = await createMint(
rpc,
payer,
payer.publicKey, // mint authority
9, // decimals
Keypair.generate(), // mint keypair
undefined, // confirm options
undefined, // token program ID
freezeAuthority.publicKey, // freeze authority
);
```
# Next Steps
# Approve and Revoke Delegate Authority
Source: https://www.zkcompression.com/compressed-tokens/guides/delegate
Grant and remove delegate spending authority for compressed tokens with approve() and revoke(). Only the token owner can perform these operations.
The `approve()` and `revoke()` functions grant and remove delegate spending authority for compressed tokens. Only the token owner can perform these instructions.
```typescript approve() theme={null}
// Approve delegate for spending up to the specified amount
const approveSignature = await approve(
rpc,
payer,
mint, // SPL mint with interface PDA for compression
amount,
owner,
delegate.publicKey, // delegate account
);
```
```typescript revoke() theme={null}
// Get delegated accounts for revocation
const delegatedAccounts = await rpc.getCompressedTokenAccountsByDelegate(
delegate.publicKey,
{ mint }
);
// Revoke delegate authority
const revokeSignature = await revoke(
rpc,
payer,
delegatedAccounts.items, // delegated compressed token accounts
owner,
);
```
## Get Started
### Approve / Revoke Delegates
Install packages in your working directory:
```bash theme={null}
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
# npm
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# yarn
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# pnpm
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
```
Install the CLI globally:
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
# start local test-validator in a separate terminal
light test-validator
```
In the code examples, use `createRpc()` without arguments for localnet.
Get an API key from [Helius](https://helius.dev) and add to `.env`:
```bash title=".env" theme={null}
API_KEY=
```
In the code examples, use `createRpc(RPC_URL)` with the devnet URL.
```typescript theme={null}
import "dotenv/config";
import { Keypair } from "@solana/web3.js";
import { createRpc } from "@lightprotocol/stateless.js";
import { createMint, mintTo, approve } from "@lightprotocol/compressed-token";
import BN from "bn.js";
import { homedir } from "os";
import { readFileSync } from "fs";
// devnet:
const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
// localnet:
// const RPC_URL = undefined;
const payer = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
)
);
(async function () {
// devnet:
const rpc = createRpc(RPC_URL);
// localnet:
// const rpc = createRpc();
// Setup: Create mint and mint tokens
const { mint } = await createMint(rpc, payer, payer.publicKey, 9);
const owner = Keypair.generate();
await mintTo(rpc, payer, mint, owner.publicKey, payer, 1_000_000_000);
// Approve delegate
const delegate = Keypair.generate();
const tx = await approve(rpc, payer, mint, new BN(500_000_000), owner, delegate.publicKey);
console.log("Mint:", mint.toBase58());
console.log("Delegate:", delegate.publicKey.toBase58());
console.log("Tx:", tx);
})();
```
```typescript theme={null}
import "dotenv/config";
import { Keypair } from "@solana/web3.js";
import { createRpc } from "@lightprotocol/stateless.js";
import { createMint, mintTo, approve, revoke } from "@lightprotocol/compressed-token";
import BN from "bn.js";
import { homedir } from "os";
import { readFileSync } from "fs";
// devnet:
const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
// localnet:
// const RPC_URL = undefined;
const payer = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
)
);
(async function () {
// devnet:
const rpc = createRpc(RPC_URL);
// localnet:
// const rpc = createRpc();
// Setup: Create mint, mint tokens, and approve delegate
const { mint } = await createMint(rpc, payer, payer.publicKey, 9);
const owner = Keypair.generate();
await mintTo(rpc, payer, mint, owner.publicKey, payer, 1_000_000_000);
const delegate = Keypair.generate();
await approve(rpc, payer, mint, new BN(500_000_000), owner, delegate.publicKey);
// Get delegated accounts and revoke
const delegatedAccounts = await rpc.getCompressedTokenAccountsByDelegate(delegate.publicKey, { mint });
const tx = await revoke(rpc, payer, delegatedAccounts.items, owner);
console.log("Mint:", mint.toBase58());
console.log("Tx:", tx);
})();
```
Before we approve or revoke delegates, we need:
* compressed token accounts to delegate or revoke delegation from, and
* an SPL mint with a token pool for compression. This token pool can be created for new SPL mints via [`createMint()`](/compressed-tokens/guides/create-mint-with-token-pool) or added to existing SPL mints via [`createTokenPool()`](/compressed-tokens/guides/add-token-pools-to-mint-accounts).
# Troubleshooting
Attempting to revoke non-delegated accounts.
```typescript theme={null}
/// Verify accounts are delegated before revocation.
const delegateAccounts = await rpc.getCompressedTokenAccountsByDelegate(
delegate.publicKey,
{ mint }
);
if (delegateAccounts.items.length === 0) {
console.log("No delegated accounts to revoke");
return;
}
```
# Advanced Configuration
```typescript theme={null}
const delegates = [
Keypair.generate().publicKey,
Keypair.generate().publicKey,
];
const amounts = [
200_000_000, // 0.2 tokens to first delegate
300_000_000, // 0.3 tokens to second delegate
];
// Approve each delegate
for (let i = 0; i < delegates.length; i++) {
const approveTx = await approve(
rpc,
payer,
mint,
amounts[i],
tokenOwner,
delegates[i],
);
console.log(`Delegate ${i + 1} approved:`, approveTx);
}
```
```typescript theme={null}
const delegates = [
new PublicKey("DELEGATE_1_ADDRESS"),
new PublicKey("DELEGATE_2_ADDRESS"),
];
// Revoke each delegate
for (const delegate of delegates) {
// Get delegated accounts for this delegate
const delegateAccounts = await rpc.getCompressedTokenAccountsByDelegate(
delegate,
{ mint }
);
if (delegateAccounts.items.length > 0) {
const revokeTx = await revoke(
rpc,
payer,
delegateAccounts.items,
tokenOwner,
);
console.log(`Delegate ${delegate.toBase58()} revoked:`, revokeTx);
}
}
```
# Next Steps
# Merge Compressed Token Accounts
Source: https://www.zkcompression.com/compressed-tokens/guides/merge-compressed-token-accounts
Consolidate multiple compressed token accounts of the same mint into a single account to reduce fragmentation.
The `mergeTokenAccounts()` function consolidates multiple compressed accounts of the same mint into a single compressed account.
The function
1. consumes multiple compressed token accounts (up to 8 accounts), and
2. creates a single output compressed token account with combined balance for the owner.
State trees where compressed account's are stored, are append only. `mergeTokenAccounts()` reduces account fragmentation to simplify balance calculations from `getCompressedTokenAccountsByOwner`.
```typescript function-merge-accounts.ts theme={null}
// Combines multiple compressed token accounts into single compressed account
const transactionSignature = await mergeTokenAccounts(
rpc,
payer,
mint, // SPL mint with interface PDA for compression
owner,
);
```
## Get Started
### Merge Compressed Accounts
Install packages in your working directory:
```bash theme={null}
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
# npm
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# yarn
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# pnpm
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
```
Install the CLI globally:
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
# start local test-validator in a separate terminal
light test-validator
```
In the code examples, use `createRpc()` without arguments for localnet.
Get an API key from [Helius](https://helius.dev) and add to `.env`:
```bash title=".env" theme={null}
API_KEY=
```
In the code examples, use `createRpc(RPC_URL)` with the devnet URL.
```typescript theme={null}
import "dotenv/config";
import { Keypair } from "@solana/web3.js";
import { createRpc, bn } from "@lightprotocol/stateless.js";
import { createMint, mintTo, mergeTokenAccounts } from "@lightprotocol/compressed-token";
import { homedir } from "os";
import { readFileSync } from "fs";
// devnet:
const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
// localnet:
// const RPC_URL = undefined;
const payer = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
)
);
(async function () {
// devnet:
const rpc = createRpc(RPC_URL);
// localnet:
// const rpc = createRpc();
// Setup: Create mint and mint multiple times to create multiple accounts
const { mint } = await createMint(rpc, payer, payer.publicKey, 9);
const owner = Keypair.generate();
await mintTo(rpc, payer, mint, owner.publicKey, payer, bn(100_000_000));
await mintTo(rpc, payer, mint, owner.publicKey, payer, bn(200_000_000));
await mintTo(rpc, payer, mint, owner.publicKey, payer, bn(300_000_000));
// Merge all accounts for owner
const tx = await mergeTokenAccounts(rpc, payer, mint, owner);
console.log("Mint:", mint.toBase58());
console.log("Tx:", tx);
})();
```
Before we merge compressed accounts, we need
* Multiple compressed token accounts of the same mint owned by the same wallet, and
* an SPL mint with a token pool for compression. This token pool can be created for new SPL mints via [`createMint()`](/compressed-tokens/guides/create-mint-with-token-pool) or added to existing SPL mints via [`createTokenPool()`](/compressed-tokens/guides/add-token-pools-to-mint-accounts).
# Troubleshooting
The owner has no compressed token accounts for the specified mint:
```typescript theme={null}
// Check if accounts exist before merging
const accounts = await rpc.getCompressedTokenAccountsByOwner(
owner.publicKey,
{ mint }
);
if (accounts.items.length === 0) {
console.log("No compressed token accounts found for this mint");
console.log("Mint address:", mint.toBase58());
console.log("Owner address:", owner.publicKey.toBase58());
return;
}
console.log(`Found ${accounts.items.length} accounts to merge`);
```
# Advanced Configuration
```typescript theme={null}
// Get account count
const accounts = await rpc.getCompressedTokenAccountsByOwner(
owner.publicKey,
{ mint }
);
// Only merge if more than 2 accounts
if (accounts.items.length > 2) {
console.log(`Merging ${accounts.items.length} accounts...`);
const mergeTx = await mergeTokenAccounts(
rpc,
payer,
mint,
tokenOwner,
);
console.log("Merge completed:", mergeTx);
} else {
console.log("Merge not needed - optimal account structure");
}
```
```typescript theme={null}
const mints = [
new PublicKey("MINT_1_ADDRESS"),
new PublicKey("MINT_2_ADDRESS"),
];
// Merge accounts for each mint
for (const mint of mints) {
console.log(`Merging accounts for mint: ${mint.toBase58()}`);
const mergeTx = await mergeTokenAccounts(
rpc,
payer,
mint,
tokenOwner,
);
console.log(`Merge completed: ${mergeTx}`);
}
```
# Next Steps
# Mint Compressed Tokens
Source: https://www.zkcompression.com/compressed-tokens/guides/mint-compressed-tokens
Mint tokens to compressed token accounts for recipients and increases a mint's token supply. Only the mint authority can perform this operation.
```typescript theme={null}
// Mint compressed tokens - mints SPL tokens to pool, creates compressed token accounts
const transactionSignature = await mintTo(
rpc,
payer,
mint, // SPL mint with interface PDA for compression
recipient, // recipient address (toPubkey parameter)
payer, // mint authority
amount,
);
```
## Get Started
### Mint Compressed Tokens
Install packages in your working directory:
```bash theme={null}
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
# npm
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# yarn
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# pnpm
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
```
Install the CLI globally:
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
# start local test-validator in a separate terminal
light test-validator
```
In the code examples, use `createRpc()` without arguments for localnet.
Get an API key from [Helius](https://helius.dev) and add to `.env`:
```bash title=".env" theme={null}
API_KEY=
```
In the code examples, use `createRpc(RPC_URL)` with the devnet URL.
```typescript theme={null}
import "dotenv/config";
import { Keypair } from "@solana/web3.js";
import { createRpc } from "@lightprotocol/stateless.js";
import { createMint, mintTo } from "@lightprotocol/compressed-token";
import { homedir } from "os";
import { readFileSync } from "fs";
// devnet:
const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
// localnet:
// const RPC_URL = undefined;
const payer = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
)
);
(async function () {
// devnet:
const rpc = createRpc(RPC_URL);
// localnet:
// const rpc = createRpc();
// Setup: Create mint
const { mint } = await createMint(rpc, payer, payer.publicKey, 9);
// Mint compressed tokens
const recipient = Keypair.generate();
const tx = await mintTo(rpc, payer, mint, recipient.publicKey, payer, 1_000_000_000);
console.log("Mint:", mint.toBase58());
console.log("Recipient:", recipient.publicKey.toBase58());
console.log("Tx:", tx);
})();
```
```typescript theme={null}
import "dotenv/config";
import { Keypair, ComputeBudgetProgram, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
import { createRpc, bn, DerivationMode } from "@lightprotocol/stateless.js";
import {
createMintInterface,
createAtaInterface,
createMintToInterfaceInstruction,
getMintInterface,
getAssociatedTokenAddressInterface,
} from "@lightprotocol/compressed-token";
import { homedir } from "os";
import { readFileSync } from "fs";
// devnet:
const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
const rpc = createRpc(RPC_URL);
// localnet:
// const rpc = createRpc();
const payer = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
)
);
(async function () {
const { mint } = await createMintInterface(rpc, payer, payer, null, 9);
const recipient = Keypair.generate();
await createAtaInterface(rpc, payer, mint, recipient.publicKey);
const destination = getAssociatedTokenAddressInterface(mint, recipient.publicKey);
const mintInterface = await getMintInterface(rpc, mint);
let validityProof;
if (mintInterface.merkleContext) {
validityProof = await rpc.getValidityProofV2(
[
{
hash: bn(mintInterface.merkleContext.hash),
leafIndex: mintInterface.merkleContext.leafIndex,
treeInfo: mintInterface.merkleContext.treeInfo,
proveByIndex: mintInterface.merkleContext.proveByIndex,
},
],
[],
DerivationMode.compressible
);
}
const ix = createMintToInterfaceInstruction(
mintInterface,
destination,
payer.publicKey,
payer.publicKey,
1_000_000_000,
validityProof
);
const tx = new Transaction().add(
ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }),
ix
);
const signature = await sendAndConfirmTransaction(rpc, tx, [payer]);
console.log("Mint:", mint.toBase58());
console.log("Tx:", signature);
})();
```
The SPL mint must have an SPL Interface PDA for compression.
The script creates it for you.
For development, create a new mint with SPL interface via [`createMint()`](https://github.com/Lightprotocol/examples-zk-compression/tree/main/compressed-token-cookbook) or add an SPL interface to an existing mint via [`createSplInterface()`](https://github.com/Lightprotocol/examples-zk-compression/tree/main/compressed-token-cookbook).
# Troubleshooting
```typescript theme={null}
// Error message: "TokenPool not found. Please create a compressed token
// pool for mint: [ADDRESS] via createTokenPool().
```
The mint does not have an interface PDA for compression. Ensure you created the mint using `createMint`.
```typescript theme={null}
// Create mint with interface PDA for compression
import { createMint } from '@lightprotocol/compressed-token';
const { mint } = await createMint(rpc, payer, payer.publicKey, 9);
```
The interface PDA info doesn't correspond to the mint address. Ensure you're fetching the correct PDA:
```typescript theme={null}
// Get the correct interface PDA for your mint
const tokenPoolInfo = await getTokenPoolInfos(rpc, mint);
```
When minting to multiple recipients, ensure arrays are the same size.
```typescript theme={null}
// Wrong: Mismatched array lengths
const recipients = [addr1, addr2, addr3];
const amounts = [100, 200]; // Only 2 amounts for 3 recipients
// Correct: Same length arrays
const recipients = [addr1, addr2, addr3];
const amounts = [100, 200, 300]; // 3 amounts for 3 recipients
```
# Advanced Configuration
```typescript theme={null}
// Mint different amounts to multiple recipients
const recipients = [
Keypair.generate().publicKey,
Keypair.generate().publicKey,
Keypair.generate().publicKey,
];
const amounts = [
1_000_000_000, // 1 token
2_000_000_000, // 2 tokens
500_000_000, // 0.5 tokens
];
const transactionSignature = await mintTo(
rpc,
payer,
mint, // SPL mint with interface PDA for compression
recipients, // array of recipients (toPubkey parameter)
payer, // mint authority
amounts, // array of amounts (amount parameter)
);
```
Mint tokens using a custom mint authority with `approveAndMintTo()`:
```typescript theme={null}
import { approveAndMintTo } from '@lightprotocol/compressed-token';
// Mint tokens with a separate mint authority
const transactionSignature = await approveAndMintTo(
rpc,
payer,
mint, // SPL mint with interface PDA for compression
recipient.publicKey, // recipient of minted tokens (toPubkey parameter)
mintAuthority, // mint authority
mintAmount,
);
```
# Next Steps
# Transfer Compressed Tokens
Source: https://www.zkcompression.com/compressed-tokens/guides/transfer-compressed-tokens
Transfer compressed tokens between accounts. Transfers consume input accounts and create new output accounts with updated balances.
The `transfer()` function moves compressed tokens between accounts.
* SPL token transfers that update the existing account
* Compressed token transfers consume input accounts from the sender and create new output accounts for sender and recipient with updated balances.
SPL token accounts can be compressed in the same transaction with `compress_or_decompress_amount`, if needed.
```typescript function-transfer-compressed-tokens.ts theme={null}
// Transfer compressed tokens
const transactionSignature = await transfer(
rpc,
payer,
mint, // SPL mint with interface PDA for compression
amount,
payer,
recipient, // destination address (toAddress parameter)
)
```
## Get Started
### Transfer Compressed Tokens
Install packages in your working directory:
```bash theme={null}
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
Install the CLI globally:
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
Install packages in your working directory:
```bash theme={null}
# npm
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# yarn
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# pnpm
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
```
Install the CLI globally:
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
# start local test-validator in a separate terminal
light test-validator
```
In the code examples, use `createRpc()` without arguments for localnet.
Get an API key from [Helius](https://helius.dev) and add to `.env`:
```bash title=".env" theme={null}
API_KEY=
```
In the code examples, use `createRpc(RPC_URL)` with the devnet URL.
```typescript theme={null}
import "dotenv/config";
import { Keypair } from "@solana/web3.js";
import { createRpc } from "@lightprotocol/stateless.js";
import { createMint, mintTo, transfer } from "@lightprotocol/compressed-token";
import { homedir } from "os";
import { readFileSync } from "fs";
// devnet:
const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
// localnet:
// const RPC_URL = undefined;
const payer = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
)
);
(async function () {
// devnet:
const rpc = createRpc(RPC_URL);
// localnet:
// const rpc = createRpc();
// Setup: Create mint and mint tokens
const { mint } = await createMint(rpc, payer, payer.publicKey, 9);
const sender = Keypair.generate();
await mintTo(rpc, payer, mint, sender.publicKey, payer, 1_000_000_000);
// Transfer compressed tokens
const recipient = Keypair.generate();
const tx = await transfer(rpc, payer, mint, 500_000_000, sender, recipient.publicKey);
console.log("Mint:", mint.toBase58());
console.log("Recipient:", recipient.publicKey.toBase58());
console.log("Tx:", tx);
})();
```
The SPL mint must have an SPL Interface PDA for compression.
The script creates it for you.
For development, create a new mint with SPL interface via [`createMint()`](https://github.com/Lightprotocol/examples-zk-compression/tree/main/compressed-token-cookbook) or add an SPL interface to an existing mint via [`createSplInterface()`](https://github.com/Lightprotocol/examples-zk-compression/tree/main/compressed-token-cookbook).
# Troubleshooting
The sender doesn't have enough compressed tokens for the requested transfer amount.
```typescript theme={null}
// Check current balance first
const tokenAccounts = await rpc.getCompressedTokenAccountsByOwner(
owner.publicKey,
{ mint }
);
if (tokenAccounts.items.length === 0) {
throw new Error("No compressed token accounts found");
}
// Calculate total balance across all accounts
const totalBalance = tokenAccounts.items.reduce(
(sum, account) => sum.add(account.parsed.amount),
new BN(0)
);
console.log("Available balance:", totalBalance.toString());
// Ensure transfer amount doesn't exceed balance
if (new BN(transferAmount).gt(totalBalance)) {
throw new Error(`Transfer amount ${transferAmount} exceeds balance ${totalBalance.toString()}`);
}
```
The transfer requires more than 4 compressed accounts, which exceeds the transaction limit.
```typescript theme={null}
// Error message: "Account limit exceeded: max X (4 accounts) per transaction.
// Total balance: Y (Z accounts). Consider multiple transfers to spend full balance."
// Split into multiple smaller transfers
const maxTransferPerTx = 1_000_000_000; // Adjust based on your account sizes
if (transferAmount > maxTransferPerTx) {
console.log("Large transfer detected, splitting into multiple transactions...");
let remainingAmount = transferAmount;
while (remainingAmount > 0) {
const currentTransfer = Math.min(remainingAmount, maxTransferPerTx);
await transfer(
rpc,
payer,
mint,
currentTransfer,
owner,
recipient
);
remainingAmount -= currentTransfer;
console.log(`Transferred ${currentTransfer}, remaining: ${remainingAmount}`);
}
}
```
# Advanced Configuration
Transfer to multiple recipients in separate transactions:
```typescript theme={null}
const recipients = [
Keypair.generate().publicKey,
Keypair.generate().publicKey,
Keypair.generate().publicKey,
];
const amounts = [
100_000_000, // 0.1 tokens
200_000_000, // 0.2 tokens
150_000_000, // 0.15 tokens
];
for (let i = 0; i < recipients.length; i++) {
const transactionSignature = await transfer(
rpc,
payer,
mint,
amounts[i],
owner,
recipients[i],
);
console.log(`Transfer ${i + 1} completed:`, transactionSignature);
}
```
Transfer tokens using delegate authority:
```typescript theme={null}
import { approve, transferDelegated } from '@lightprotocol/compressed-token';
// 1. Owner approves delegate
await approve(
rpc,
payer,
mint,
amount,
owner, // Signer
delegate.publicKey, // PublicKey
);
// 2. Delegate transfers tokens
await transferDelegated(
rpc,
payer,
mint,
transferAmount,
delegate, // Signer - named "owner" in SDK
recipient,
);
```
# Next Steps
# Overview
Source: https://www.zkcompression.com/compressed-tokens/overview
Overview to compressed tokens and guides with full code examples. Use for distributing tokens (eg rewards, airdrops) or creating associated token accounts for users.
**Compressed Tokens are supported by leading Solana wallets such as Phantom and Backpack.**
| Creation | Solana | Compressed |
| :---------------- | :------------------- | :----------------- |
| **Token Account** | \~2,000,000 lamports | **5,000** lamports |
1. Compressed token accounts store token balance, owner, and other information for SPL and Token 2022 mints.
2. Compressed token accounts are rent-free.
3. Any SPL or Token 2022 token can be compressed and decompressed at will.
## Recommended Usage of Compressed Tokens
Distribute tokens without paying upfront rent per recipient.
Compress existing SPL-token accounts to save rent costs for your users.
# Get Started
### Install dependencies
```bash theme={null}
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
```bash theme={null}
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
```bash theme={null}
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
```bash theme={null}
# npm
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# yarn
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# pnpm
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
```
### Set up your developer environment
By default, all guides use Localnet.
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
# Start a local test validator
light test-validator
## ensure you have the Solana CLI accessible in your system PATH
```
```typescript theme={null}
// createRpc() defaults to local test validator endpoints
import {
Rpc,
createRpc,
} from "@lightprotocol/stateless.js";
const connection: Rpc = createRpc();
async function main() {
let slot = await connection.getSlot();
console.log(slot);
let health = await connection.getIndexerHealth(slot);
console.log(health);
// "Ok"
}
main();
```
Replace `` with your actual API key. [Get your API key here](https://www.helius.dev/zk-compression), if you don't have one yet.
```typescript theme={null}
import { createRpc } from "@lightprotocol/stateless.js";
const RPC_ENDPOINT = "https://devnet.helius-rpc.com?api-key=";
const connection = createRpc(RPC_ENDPOINT);
async function main() {
let slot = await connection.getSlot();
console.log(slot);
let health = await connection.getIndexerHealth(slot);
console.log(health);
// "Ok"
}
main();
```
### Basic Guides
| Guide | Description |
| :-------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------- |
| [Create Compressed Token Accounts](https://github.com/Lightprotocol/examples-zk-compression/tree/main/compressed-token-cookbook) | Create compressed and learn difference to regular token accounts |
| [Mint Compressed Tokens](https://github.com/Lightprotocol/examples-zk-compression/tree/main/compressed-token-cookbook) | Create new compressed tokens to existing mint |
| [Transfer Compressed Tokens](https://github.com/Lightprotocol/examples-zk-compression/tree/main/compressed-token-cookbook) | Move compressed tokens between compressed accounts |
| [Decompress and Compress Tokens](https://github.com/Lightprotocol/examples-zk-compression/tree/main/compressed-token-cookbook) | Convert SPL tokens between regular and compressed format |
| [Compress Complete SPL Token Accounts](https://github.com/Lightprotocol/examples-zk-compression/tree/main/compressed-token-cookbook) | Compress complete SPL token accounts and reclaim rent afterwards |
| [Create a Mint with Interface PDA](https://github.com/Lightprotocol/examples-zk-compression/tree/main/compressed-token-cookbook) | Create new SPL mint with SPL Interface PDA for compression |
| [Create SPL Interface for Existing Mints](https://github.com/Lightprotocol/examples-zk-compression/tree/main/compressed-token-cookbook) | Create SPL Interface PDA for existing SPL mints |
| [Merge Compressed Accounts](https://github.com/Lightprotocol/examples-zk-compression/tree/main/compressed-token-cookbook) | Consolidate multiple compressed accounts of the same mint into one |
| [Approve and Revoke Delegate Authority](https://github.com/Lightprotocol/examples-zk-compression/tree/main/compressed-token-cookbook) | Approve or revoke delegates for compressed token accounts |
### Advanced Guides
| Guide | Description |
| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| [Create an Airdrop without Claim](/compressed-tokens/token-distribution) | ZK Compression is the most efficient way to distribute SPL tokens. Distribute via Webapp or customize with claim. |
| [Privy Guide](/compressed-tokens/privy) | Integrate compressed tokens with Privy embedded wallets for rent-free token accounts |
# Rent-Free SPL Accounts with Privy
Source: https://www.zkcompression.com/compressed-tokens/privy
Integrate compressed tokens with Privy embedded wallets for rent-free SPL token accounts and transfers.
| Creation Cost | SPL Token Account | Compressed Token |
| :-------------- | :------------------- | :------------------- |
| **Per account** | \~2,000,000 lamports | \~**5,000** lamports |
Privy handles user authentication and wallet management. You build transactions with tokens (SOL, SPL or compressed) and Privy signs them client-side:
1. Authenticate with Privy
2. Build unsigned transaction
3. Sign transaction using Privy's wallet provider
4. Send signed transaction to RPC
## What you will implement
|
SPL |
Compressed |
| **Get Balance** |
getAccount() |
getCompressedTokenAccountsByOwner() |
| **Transfer** |
transferChecked() |
transfer() |
| **Compress** |
N/A |
compress() |
| **Decompress** |
N/A |
decompress() |
| **Transaction History** |
getSignaturesForAddress() |
getCompressionSignaturesForOwner() |
### Prerequisites
```bash theme={null}
npm install @lightprotocol/compressed-token@^0.23.0 \
@lightprotocol/stateless.js@^0.23.0
```
```bash theme={null}
npm install @lightprotocol/token-interface@^0.1.2 \
@lightprotocol/stateless.js@^0.23.0
```
Connect to an RPC endpoint that supports ZK Compression (Helius, Triton)
```typescript theme={null}
import { createRpc } from "@lightprotocol/stateless.js";
import {
transfer,
compress,
decompress,
selectMinCompressedTokenAccountsForTransfer,
} from "@lightprotocol/compressed-token";
const rpc = createRpc(RPC_ENDPOINT);
```
Before we can compress or decompresss, we need:
* An SPL mint with an interface PDA for compression. This interface PDA can be created for new SPL mints via [`createMint()`](/compressed-tokens/guides/create-mint-with-token-pool) or added to existing SPL mints via [`createTokenPool()`](/compressed-tokens/guides/add-token-pools-to-mint-accounts).
* For `compress()` SPL tokens in an Associated Token Account, or
* For `decompress()` compressed token accounts with sufficient balance.
```typescript theme={null}
import "dotenv/config";
import { Keypair } from "@solana/web3.js";
import { createRpc } from "@lightprotocol/stateless.js";
import { createMint } from "@lightprotocol/compressed-token";
import { homedir } from "os";
import { readFileSync } from "fs";
// devnet:
const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
// localnet:
// const RPC_URL = undefined;
const payer = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
)
);
(async function () {
// devnet:
const rpc = createRpc(RPC_URL);
// localnet:
// const rpc = createRpc();
const { mint, transactionSignature } = await createMint(
rpc,
payer,
payer.publicKey,
9
);
console.log("Mint:", mint.toBase58());
console.log("Tx:", transactionSignature);
})();
```
## Full Code Examples
Find a complete example on Github: [privy/nodejs-privy-compressed](https://github.com/Lightprotocol/examples-zk-compression/tree/main/privy).
Send compressed tokens to another recipient, similar to SPL token transfers:
1. Fetch compressed token accounts with `getCompressedTokenAccountsByOwner`
2. Select sender accounts with `selectMinCompressedTokenAccountsForTransfer`
3. Fetch a validity proof from your RPC provider to prove the account's state correctness
4. Build the transfer instruction and transaction
5. Sign with Privy and send transaction
```typescript theme={null}
import 'dotenv/config';
import {PrivyClient} from '@privy-io/node';
import {createRpc, bn} from '@lightprotocol/stateless.js';
import {PublicKey, Transaction, ComputeBudgetProgram} from '@solana/web3.js';
import {CompressedTokenProgram, selectMinCompressedTokenAccountsForTransfer} from '@lightprotocol/compressed-token';
const transferCompressedTokens = async (
fromAddress: string,
toAddress: string,
tokenMintAddress: string,
amount: number,
decimals: number = 6
) => {
const connection = createRpc(process.env.HELIUS_RPC_URL!);
const privy = new PrivyClient({
appId: process.env.PRIVY_APP_ID!,
appSecret: process.env.PRIVY_APP_SECRET!,
});
// Create public key objects
const fromPubkey = new PublicKey(fromAddress);
const toPubkey = new PublicKey(toAddress);
const mintPubkey = new PublicKey(tokenMintAddress);
const tokenAmount = bn(amount * Math.pow(10, decimals));
// Get compressed token accounts (filter out null items from indexer)
const accounts = await connection.getCompressedTokenAccountsByOwner(fromPubkey, {mint: mintPubkey});
const validItems = (accounts.items || []).filter((item): item is NonNullable => item !== null);
if (validItems.length === 0) {
throw new Error('No compressed token accounts found');
}
// Select minimum accounts needed for transfer
const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer(validItems, tokenAmount);
if (inputAccounts.length === 0) {
throw new Error('Insufficient balance');
}
// Get validity proof to prove compressed token accounts exist in state tree.
const proof = await connection.getValidityProof(inputAccounts.map(account => bn(account.compressedAccount.hash)));
// Create transfer instruction
const instruction = await CompressedTokenProgram.transfer({
payer: fromPubkey,
inputCompressedTokenAccounts: inputAccounts,
toAddress: toPubkey,
amount: tokenAmount,
recentInputStateRootIndices: proof.rootIndices,
recentValidityProof: proof.compressedProof,
});
// Create transaction
const transaction = new Transaction();
transaction.add(ComputeBudgetProgram.setComputeUnitLimit({units: 300_000}));
transaction.add(instruction);
// Get recent blockhash
const {blockhash} = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = fromPubkey;
// Sign with Privy
const signResult = await privy.wallets().solana().signTransaction(process.env.TREASURY_WALLET_ID!, {
transaction: transaction.serialize({requireAllSignatures: false}),
authorization_context: {
authorization_private_keys: [process.env.TREASURY_AUTHORIZATION_KEY!]
}
});
const signedTx = (signResult as any).signed_transaction || signResult.signedTransaction;
if (!signedTx) {
throw new Error('Privy returned invalid response: ' + JSON.stringify(signResult));
}
const signedTransaction = Buffer.from(signedTx, 'base64');
// Send transaction
const signature = await connection.sendRawTransaction(signedTransaction, {
skipPreflight: false,
preflightCommitment: 'confirmed'
});
await connection.confirmTransaction(signature, 'confirmed');
return signature;
};
export default transferCompressedTokens;
```
Convert SPL to compressed tokens and send to a recipient in one instruction.
```typescript theme={null}
import 'dotenv/config';
import {PrivyClient} from '@privy-io/node';
import {createRpc, bn, selectStateTreeInfo} from '@lightprotocol/stateless.js';
import {PublicKey, Transaction, ComputeBudgetProgram} from '@solana/web3.js';
import {getAssociatedTokenAddressSync, getAccount} from '@solana/spl-token';
import {CompressedTokenProgram, getTokenPoolInfos, selectTokenPoolInfo} from '@lightprotocol/compressed-token';
const compressTokens = async (
fromAddress: string,
toAddress: string,
tokenMintAddress: string,
amount: number,
decimals: number = 6
) => {
const connection = createRpc(process.env.HELIUS_RPC_URL!);
const privy = new PrivyClient({
appId: process.env.PRIVY_APP_ID!,
appSecret: process.env.PRIVY_APP_SECRET!,
});
// Create public key objects
const fromPubkey = new PublicKey(fromAddress);
const toPubkey = new PublicKey(toAddress);
const mintPubkey = new PublicKey(tokenMintAddress);
const tokenAmount = bn(amount * Math.pow(10, decimals));
// Get source token account and verify balance
const ownerAta = getAssociatedTokenAddressSync(mintPubkey, fromPubkey);
const ataAccount = await getAccount(connection, ownerAta);
if (ataAccount.amount < BigInt(tokenAmount.toString())) {
throw new Error('Insufficient SPL balance');
}
// Get state tree to store compressed tokens
// Get token pool info. Stores SPL tokens in interface PDA when compressed.
const stateTreeInfos = await connection.getStateTreeInfos();
const selectedTreeInfo = selectStateTreeInfo(stateTreeInfos);
const tokenPoolInfos = await getTokenPoolInfos(connection, mintPubkey);
const tokenPoolInfo = selectTokenPoolInfo(tokenPoolInfos);
// Create compress instruction
const instruction = await CompressedTokenProgram.compress({
payer: fromPubkey,
owner: fromPubkey,
source: ownerAta,
toAddress: toPubkey,
mint: mintPubkey,
amount: tokenAmount,
outputStateTreeInfo: selectedTreeInfo,
tokenPoolInfo,
});
// Create transaction
const transaction = new Transaction();
transaction.add(ComputeBudgetProgram.setComputeUnitLimit({units: 300_000}));
transaction.add(instruction);
// Get recent blockhash
const {blockhash} = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = fromPubkey;
// Sign with Privy
const signResult = await privy.wallets().solana().signTransaction(process.env.TREASURY_WALLET_ID!, {
transaction: transaction.serialize({requireAllSignatures: false}),
authorization_context: {
authorization_private_keys: [process.env.TREASURY_AUTHORIZATION_KEY!]
}
});
const signedTx = (signResult as any).signed_transaction || signResult.signedTransaction;
if (!signedTx) {
throw new Error('Privy returned invalid response: ' + JSON.stringify(signResult));
}
const signedTransaction = Buffer.from(signedTx, 'base64');
// Send transaction
const signature = await connection.sendRawTransaction(signedTransaction, {
skipPreflight: false,
preflightCommitment: 'confirmed'
});
await connection.confirmTransaction(signature, 'confirmed');
return signature;
};
export default compressTokens;
```
Convert compressed tokens back to SPL tokens.
```typescript theme={null}
import 'dotenv/config';
import {PrivyClient} from '@privy-io/node';
import {createRpc} from '@lightprotocol/stateless.js';
import {Keypair, PublicKey, Transaction} from '@solana/web3.js';
import {getAssociatedTokenAddressSync, createAssociatedTokenAccount} from '@solana/spl-token';
import {decompress} from '@lightprotocol/compressed-token';
const decompressTokens = async (
fromAddress: string,
tokenMintAddress: string,
amount: number,
decimals: number = 6
) => {
const connection = createRpc(process.env.HELIUS_RPC_URL!);
const privy = new PrivyClient({
appId: process.env.PRIVY_APP_ID!,
appSecret: process.env.PRIVY_APP_SECRET!,
});
const fromPubkey = new PublicKey(fromAddress);
const mintPubkey = new PublicKey(tokenMintAddress);
const rawAmount = amount * Math.pow(10, decimals);
// Get destination ATA
const ownerAta = getAssociatedTokenAddressSync(mintPubkey, fromPubkey);
// Check ATA exists (decompress action will handle creation internally)
// But we need to be aware ATA creation requires a separate signer
// Create fake keypair for decompress action (only publicKey is used)
const dummyPayer = {
publicKey: fromPubkey,
secretKey: new Uint8Array(64),
} as any;
// Intercept sendAndConfirmTransaction to use Privy signing
const originalSendAndConfirm = (connection as any).sendAndConfirmTransaction;
(connection as any).sendAndConfirmTransaction = async (tx: Transaction, signers: any[]) => {
const signResult = await privy.wallets().solana().signTransaction(process.env.TREASURY_WALLET_ID!, {
transaction: tx.serialize({requireAllSignatures: false}),
authorization_context: {
authorization_private_keys: [process.env.TREASURY_AUTHORIZATION_KEY!]
}
});
const signedTx = (signResult as any).signed_transaction || signResult.signedTransaction;
if (!signedTx) {
throw new Error('Privy returned invalid response');
}
const signedTransaction = Buffer.from(signedTx, 'base64');
const signature = await connection.sendRawTransaction(signedTransaction, {
skipPreflight: false,
preflightCommitment: 'confirmed'
});
await connection.confirmTransaction(signature, 'confirmed');
return signature;
};
try {
// Use high-level decompress action (handles account configuration correctly)
const signature = await decompress(
connection,
dummyPayer,
mintPubkey,
rawAmount,
dummyPayer,
ownerAta
);
return signature;
} finally {
// Restore original function
(connection as any).sendAndConfirmTransaction = originalSendAndConfirm;
}
};
export default decompressTokens;
```
## Get balances
Fetch SPL and compressed token balances.
```typescript theme={null}
import {PublicKey} from '@solana/web3.js';
import {createRpc} from '@lightprotocol/stateless.js';
import {HELIUS_RPC_URL} from './config.js';
export async function getCompressedBalances(ownerAddress: string) {
const rpc = createRpc(HELIUS_RPC_URL);
const owner = new PublicKey(ownerAddress);
// Get compressed SOL balance
const compressedSol = await rpc.getCompressedBalanceByOwner(owner);
const compressedSolLamports = compressedSol.value ? BigInt(compressedSol.value.toString()) : 0n;
// Get compressed token accounts (filter out null items from indexer)
const compressedAccounts = await rpc.getCompressedTokenAccountsByOwner(owner);
const validItems = (compressedAccounts.items || []).filter((item): item is NonNullable => item !== null);
// Aggregate balances by mint
const balances = new Map();
for (const account of validItems) {
if (account.parsed) {
const mint = account.parsed.mint.toBase58();
const amount = BigInt(account.parsed.amount.toString());
const current = balances.get(mint) || 0n;
balances.set(mint, current + amount);
}
}
return {
sol: compressedSolLamports.toString(),
tokens: Array.from(balances.entries()).map(([mint, amount]) => ({
mint,
amount: amount.toString(),
accounts: validItems.filter(a => a.parsed?.mint.toBase58() === mint).length
}))
};
}
```
## Get transaction history
Fetch compressed token transaction history for an owner, with optional detailed compression information.
```typescript theme={null}
import 'dotenv/config';
import {createRpc} from '@lightprotocol/stateless.js';
import {PublicKey} from '@solana/web3.js';
const getTransactionHistory = async (
ownerAddress: string,
limit: number = 10,
includeDetails: boolean = false
) => {
const connection = createRpc(process.env.HELIUS_RPC_URL!);
const owner = new PublicKey(ownerAddress);
// Get compression signatures for token owner
const signatures = await connection.getCompressionSignaturesForTokenOwner(owner);
if (signatures.items.length === 0) {
return {
count: 0,
transactions: [],
};
}
// Limit results
const limitedSignatures = signatures.items.slice(0, limit);
// Get detailed info if requested
if (includeDetails && limitedSignatures.length > 0) {
const transactions = await Promise.all(
limitedSignatures.map(async (sig) => {
const txInfo = await connection.getTransactionWithCompressionInfo(sig.signature);
return {
signature: sig.signature,
slot: sig.slot,
blockTime: sig.blockTime,
timestamp: new Date(sig.blockTime * 1000).toISOString(),
compressionInfo: txInfo?.compressionInfo ? {
closedAccounts: txInfo.compressionInfo.closedAccounts.length,
openedAccounts: txInfo.compressionInfo.openedAccounts.length,
} : null,
};
})
);
return {
count: signatures.items.length,
transactions,
};
}
// Return basic signature info
const transactions = limitedSignatures.map((sig) => ({
signature: sig.signature,
slot: sig.slot,
blockTime: sig.blockTime,
timestamp: new Date(sig.blockTime * 1000).toISOString(),
}));
return {
count: signatures.items.length,
transactions,
};
};
export default getTransactionHistory;
```
# Use Token-2022 with Compression
Source: https://www.zkcompression.com/compressed-tokens/token-2022
Complete guide to mint, compress and transfer tokens with Token-2022 Metadata with ZK Compression.
# What you will do
With this guide you will mint, compress, and transfer tokens with Token-2022 Metadata.
# Overview Token 2022 Extensions
Token 2022 Extensions are optional features that can be added to Token 2022 mints on Solana to enable additional functionality.
ZK Compression supports compressing the following mint-extensions:
* MetadataPointer
* TokenMetadata
* InterestBearingConfig
* GroupPointer
* GroupMemberPointer
* TokenGroup
* TokenGroupMember
All other extensions are not yet supported.
If you require support for other mint-extensions, [let us know](https://t.me/swen_light)!
# Get started
You need the following SDK versions:
* `@lightprotocol/stateless.js` ≥ 0.21.0
* `@lightprotocol/compressed-token` ≥ 0.21.0
* `@solana/web3.js` ≥ 1.95.3
Make sure you have dependencies and developer environment set up!
**Dependencies**
```bash theme={null}
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
```bash theme={null}
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
```bash theme={null}
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
```bash theme={null}
# npm
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# yarn
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# pnpm
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
```
**Developer Environment**
By default, all guides use Localnet.
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
# Start a local test validator
light test-validator
## ensure you have the Solana CLI accessible in your system PATH
```
```typescript theme={null}
// createRpc() defaults to local test validator endpoints
import {
Rpc,
createRpc,
} from "@lightprotocol/stateless.js";
const connection: Rpc = createRpc();
async function main() {
let slot = await connection.getSlot();
console.log(slot);
let health = await connection.getIndexerHealth(slot);
console.log(health);
// "Ok"
}
main();
```
Replace `` with your actual API key. [Get your API key here](https://www.helius.dev/zk-compression), if you don't have one yet.
```typescript theme={null}
import { createRpc } from "@lightprotocol/stateless.js";
const RPC_ENDPOINT = "https://devnet.helius-rpc.com?api-key=";
const connection = createRpc(RPC_ENDPOINT);
async function main() {
let slot = await connection.getSlot();
console.log(slot);
let health = await connection.getIndexerHealth(slot);
console.log(health);
// "Ok"
}
main();
```
Run `compress-t22.ts`.
```typescript compress-t22.ts expandable theme={null}
// Token-2022 with ZK Compression - Local
// 1. Load wallet and connect to local validator
// 2. Create Token-2022 mint with metadata extension and register for compression via createTokenPool()
// 3. Mint SPL tokens to ATA, compress via compress(), and transfer compressed tokens via transfer()
// 4. Verify balances via getTokenAccountBalance and getCompressedTokenAccountsByOwner
import { confirmTx, createRpc } from "@lightprotocol/stateless.js";
import {
compress,
createTokenPool,
transfer,
} from "@lightprotocol/compressed-token";
import {
getOrCreateAssociatedTokenAccount,
mintTo as mintToSpl,
TOKEN_2022_PROGRAM_ID,
createInitializeMetadataPointerInstruction,
createInitializeMintInstruction,
ExtensionType,
getMintLen,
LENGTH_SIZE,
TYPE_SIZE,
} from "@solana/spl-token";
import {
Keypair,
sendAndConfirmTransaction,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import {
createInitializeInstruction,
pack,
TokenMetadata,
} from "@solana/spl-token-metadata";
// Step 1: Setup local connection and load wallet
import * as fs from 'fs';
import * as os from 'os';
const walletPath = `${os.homedir()}/.config/solana/id.json`;
const secretKey = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
const payer = Keypair.fromSecretKey(Buffer.from(secretKey));
const connection = createRpc(); // defaults to localhost:8899
(async () => {
// Generate mint keypair and define Token-2022 metadata
const mint = Keypair.generate();
const decimals = 9;
const metadata: TokenMetadata = {
mint: mint.publicKey,
name: "Local Test Token",
symbol: "LTT",
uri: "https://example.com/token-metadata.json",
additionalMetadata: [["environment", "localnet"], ["test", "true"]],
};
// Calculate space requirements for Token-2022 mint with MetadataPointer extension
const mintLen = getMintLen([ExtensionType.MetadataPointer]);
const metadataLen = TYPE_SIZE + LENGTH_SIZE + pack(metadata).length;
// Check wallet balance
const balance = await connection.getBalance(payer.publicKey);
// Step 2: Create Token-2022 mint with metadata extension
const mintLamports = await connection.getMinimumBalanceForRentExemption(
mintLen + metadataLen
);
const mintTransaction = new Transaction().add(
// Create account for Token-2022 mint
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mint.publicKey,
space: mintLen,
lamports: mintLamports,
programId: TOKEN_2022_PROGRAM_ID,
}),
// Initialize MetadataPointer extension
createInitializeMetadataPointerInstruction(
mint.publicKey,
payer.publicKey,
mint.publicKey,
TOKEN_2022_PROGRAM_ID
),
// Initialize Token-2022 mint account
createInitializeMintInstruction(
mint.publicKey,
decimals,
payer.publicKey,
null,
TOKEN_2022_PROGRAM_ID
),
// Initialize token metadata
createInitializeInstruction({
programId: TOKEN_2022_PROGRAM_ID,
mint: mint.publicKey,
metadata: mint.publicKey,
name: metadata.name,
symbol: metadata.symbol,
uri: metadata.uri,
mintAuthority: payer.publicKey,
updateAuthority: payer.publicKey,
})
);
// Send Token-2022 mint creation transaction
const mintCreationTxId = await sendAndConfirmTransaction(
connection,
mintTransaction,
[payer, mint]
);
console.log(`Token-2022 mint created with address: ${mint.publicKey.toString()}`);
// Step 3: Call createTokenPool() to initialize omnibus account
// and register Token-2022 mint with Compressed Token Program
// Create PDA that holds SPL tokens for compressed tokens
const registerTxId = await createTokenPool(
connection,
payer,
mint.publicKey, // Token-2022 mint to register with compressed token program
undefined,
TOKEN_2022_PROGRAM_ID
);
// Step 4: Create associated token account and mint tokens
const ata = await getOrCreateAssociatedTokenAccount(
connection,
payer,
mint.publicKey, // Token-2022 mint with interface PDA for compression
payer.publicKey,
undefined,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID
);
// Mint Token-2022 tokens to ATA
const mintAmount = 400_000_000; // 0.4 tokens
const mintToTxId = await mintToSpl(
connection,
payer,
mint.publicKey, // Token-2022 mint with interface PDA for compression
ata.address, // destination token account
payer.publicKey, // mint authority
mintAmount, // amount to mint
undefined, // multiSigners (not used for single authority)
undefined,
TOKEN_2022_PROGRAM_ID
);
console.log(`Minted ${mintAmount / 1e9} Token-2022 tokens with metadata extension`);
console.log(`Transaction: ${mintToTxId}`);
// Step 5: Call compress() to convert Token-2022 tokens to compressed format
// Transfer Token-2022 tokens to omnibus pool and mint compressed tokens
const compressAmount = 300_000_000; // 0.3 tokens
const compressTxId = await compress(
connection,
payer,
mint.publicKey, // Token-2022 mint with interface PDA for compression
compressAmount, // amount to compress
payer, // owner of SPL tokens
ata.address, // Source ATA for compression
payer.publicKey // recipient for compressed tokens
);
console.log(`\nCompressed ${compressAmount / 1e9} Token-2022 tokens`);
console.log(`Transaction: ${compressTxId}`);
// Step 6: Transfer compressed Token-2022 tokens
const transferRecipient = Keypair.generate(); // Keypair recipient
const transferAmount = 100_000_000; // 0.1 tokens
const transferTxId = await transfer(
connection,
payer,
mint.publicKey, // Token-2022 mint with interface PDA for compression
transferAmount,
payer,
transferRecipient.publicKey
);
console.log(`\nTransferred ${transferAmount / 1e9} Compressed Token-2022 tokens`);
console.log(`Transaction: ${transferTxId}`);
console.log(`Recipient: ${transferRecipient.publicKey.toString()}`);
// Step 7: Verify balances via getTokenAccountBalance and getCompressedTokenAccountsByOwner
const senderCompressedAccounts = await connection.getCompressedTokenAccountsByOwner(payer.publicKey, { mint: mint.publicKey });
const senderBalance = senderCompressedAccounts.items.reduce((sum, account) => sum + Number(account.parsed.amount), 0);
const recipientCompressedAccounts = await connection.getCompressedTokenAccountsByOwner(transferRecipient.publicKey, { mint: mint.publicKey });
const recipientBalance = recipientCompressedAccounts.items.reduce((sum, account) => sum + Number(account.parsed.amount), 0);
const splBalance = await connection.getTokenAccountBalance(ata.address);
console.log(`\nSummary Token-2022 balances:`);
console.log(`Sender balance: ${senderBalance / 1e9} compressed tokens / ${splBalance.value.uiAmount} SPL tokens`);
console.log(`Recipient balance: ${recipientBalance / 1e9} compressed tokens`);
})().catch(console.error);
```
# Next Steps
# Token Distribution Guide
Source: https://www.zkcompression.com/compressed-tokens/token-distribution
Complete client and program guides to create an airdrop – with or without code. ZK compression is the most efficient way to distribute SPL tokens.
***
Use the [airdrop](https://github.com/Lightprotocol/skills/tree/main/skills/airdrop) agent skill for token distribution and airdrops:
```
npx skills add Lightprotocol/skills
```
For orchestration, install the [general skill](https://zkcompression.com/skill.md):
```bash theme={null}
npx skills add https://zkcompression.com
```
Choose your implementation based on your needs:
| Tab | Best For | What You'll Get | Time |
| :------------------------------------ | :------------------------- | :------------------------------ | :----- |
| **Localnet Guide** | First-time users, learning | Step-by-step Localnet tutorial | 20 min |
| **Simple Airdrop** | \<10,000 recipients | Production-ready single script | 10 min |
| **Airdrop with Batched Instructions** | 10,000+ recipients | Batched system with retry logic | 15 min |
Via Webapp you can use [Airship by Helius Labs](https://airship.helius.dev/) to airdrop to up to 200,000 recipients.
**What you'll build:** A test airdrop sending compressed tokens to 3 recipients on your local validator.
### Prerequisites
Make sure you have dependencies and developer environment set up!
System Requirements
* **Node.js >= 20.18.0** (required by latest Solana packages)
* npm or yarn package manager
**Dependencies**
```bash theme={null}
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
```bash theme={null}
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
```bash theme={null}
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
```bash theme={null}
# npm
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# yarn
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# pnpm
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
```
**Developer Environment**
By default, all guides use Localnet.
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
# Start a local test validator
light test-validator
## ensure you have the Solana CLI accessible in your system PATH
```
```typescript theme={null}
// createRpc() defaults to local test validator endpoints
import {
Rpc,
createRpc,
} from "@lightprotocol/stateless.js";
const connection: Rpc = createRpc();
async function main() {
let slot = await connection.getSlot();
console.log(slot);
let health = await connection.getIndexerHealth(slot);
console.log(health);
// "Ok"
}
main();
```
Replace `` with your actual API key. [Get your API key here](https://www.helius.dev/zk-compression), if you don't have one yet.
```typescript theme={null}
import { createRpc } from "@lightprotocol/stateless.js";
const RPC_ENDPOINT = "https://devnet.helius-rpc.com?api-key=";
const connection = createRpc(RPC_ENDPOINT);
async function main() {
let slot = await connection.getSlot();
console.log(slot);
let health = await connection.getIndexerHealth(slot);
console.log(health);
// "Ok"
}
main();
```
### Mint SPL tokens to your wallet
Run this `mint-spl-tokens.ts` to mint SPL tokens to your wallet.
```typescript mint-spl-tokens.ts expandable theme={null}
// Mint SPL Tokens for Airdrop - LocalNet
// 1. Load wallet and connect to local validator
// 2. Create SPL mint with SPL interface via createMint()
// 3. Create ATA and mint SPL tokens to sender for airdrop preparation
// 4. Output mint address for use in simple-airdrop.ts
import { Keypair } from "@solana/web3.js";
import { createRpc } from "@lightprotocol/stateless.js";
import {
createMint,
getOrCreateAssociatedTokenAccount,
mintTo,
} from "@solana/spl-token";
import { createSplInterface } from "@lightprotocol/compressed-token";
import * as fs from 'fs';
import * as os from 'os';
// Step 1: Setup local connection and load wallet
const connection = createRpc(); // defaults to localhost:8899
// Load wallet from filesystem
const walletPath = `${os.homedir()}/.config/solana/id.json`;
const secretKey = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
const payer = Keypair.fromSecretKey(Buffer.from(secretKey));
(async () => {
// Step 2: Create SPL mint with SPL interface
const mint = await createMint(connection, payer, payer.publicKey, null, 9);
const poolTxId = await createSplInterface(connection, payer, mint);
console.log(`Mint address: ${mint.toBase58()}`);
console.log(`SPL interface created: ${poolTxId}`);
// Step 3: Create associated token account for sender
// The sender will send tokens from this account to the recipients as compressed tokens.
const ata = await getOrCreateAssociatedTokenAccount(
connection,
payer,
mint, // SPL mint with SPL interface for compression
payer.publicKey
);
console.log(`ATA address: ${ata.address.toBase58()}`);
// Step 4: Mint SPL tokens to ATA.
// The sender will send tokens from this account to the recipients as compressed tokens.
const mintToTxId = await mintTo(
connection,
payer,
mint, // SPL mint with SPL interface for compression
ata.address, // distributor ATA
payer.publicKey,
100_000_000_000 // amount: 100 tokens with 9 decimals
);
console.log(`\nSPL tokens minted and ready for distribution!`);
console.log(`Transaction: ${mintToTxId}`);
console.log(`\nCopy mint address to your airdrop script: ${mint.toBase58()}`);
})();
```
### Execute the Airdrop
Next, distribute the SPL tokens to all recipients.
Ensure you have the latest `@lightprotocol/stateless.js` and `@lightprotocol/compressed-token` versions `≥ 0.21.0`!
```typescript simple-airdrop.ts expandable highlight={29-30,45,74,100} theme={null}
// Simple Airdrop - LocalNet
// 1. Load wallet and select compression infrastructure with getStateTreeInfos() and getTokenPoolInfos()
// 2. Build LightTokenProgram.compress() instruction for multiple recipients in one transaction
// 3. Execute transaction with compute budget and confirm compression operation with sendAndConfirmTx()
// 4. Verify distribution via getCompressedTokenAccountsByOwner
import { Keypair, PublicKey, ComputeBudgetProgram } from "@solana/web3.js";
import {
LightTokenProgram,
getTokenPoolInfos,
selectTokenPoolInfo,
} from "@lightprotocol/compressed-token";
import {
bn,
buildAndSignTx,
calculateComputeUnitPrice,
createRpc,
dedupeSigner,
Rpc,
selectStateTreeInfo,
sendAndConfirmTx,
} from "@lightprotocol/stateless.js";
import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token";
import * as fs from 'fs';
import * as os from 'os';
// Step 1: Setup local connection and load wallet
const connection: Rpc = createRpc(); // defaults to localhost:8899
const mint = new PublicKey("MINTADDRESS"); // Replace with mint address from mint-spl-tokens.ts
// Local uses file wallet. Use constants from .env file in production
const walletPath = `${os.homedir()}/.config/solana/id.json`;
const secretKey = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
const payer = Keypair.fromSecretKey(Buffer.from(secretKey));
const owner = payer;
(async () => {
// Step 2: Select state tree and SPL interface
const activeStateTrees = await connection.getStateTreeInfos();
const treeInfo = selectStateTreeInfo(activeStateTrees);
const infos = await getTokenPoolInfos(connection, mint);
const info = selectTokenPoolInfo(infos);
// Step 3: Get or create source token account for distribution
// The sender will send tokens from this account to the recipients as compressed tokens.
const sourceTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
payer,
mint, // SPL mint with SPL interface for compression
payer.publicKey
);
// Step 4: Define airdrop recipients and amounts
const airDropAddresses = [
Keypair.generate().publicKey,
Keypair.generate().publicKey,
Keypair.generate().publicKey,
];
const amounts = [
bn(20_000_000_000), // 20 tokens
bn(30_000_000_000), // 30 tokens
bn(40_000_000_000), // 40 tokens
];
const totalAmount = amounts.reduce((sum, amt) => sum + amt.toNumber(), 0);
console.log(`Distributing ${totalAmount / 1e9} compressed tokens to ${airDropAddresses.length} recipients`);
const initialSplBalance = await connection.getTokenAccountBalance(sourceTokenAccount.address);
console.log(`Sender initial balance: ${initialSplBalance.value.uiAmount} tokens`);
// Step 5: Build transaction with compute budget and compression instruction
const instructions = [];
// Set compute unit limits based on recipient count (estimated 120k CU per recipient)
instructions.push(
ComputeBudgetProgram.setComputeUnitLimit({ units: 120_000 * airDropAddresses.length }),
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: calculateComputeUnitPrice(20_000, 120_000 * airDropAddresses.length), // dynamic priority fee
})
);
// Create compression instruction for multiple recipients in one transaction
const compressInstruction = await LightTokenProgram.compress({
payer: payer.publicKey,
owner: owner.publicKey,
source: sourceTokenAccount.address, // source ATA holding SPL tokens
toAddress: airDropAddresses, // recipient addresses for compressed tokens
amount: amounts, // different amounts for each recipient
mint, // SPL mint with SPL interface for compression
tokenPoolInfo: info,
outputStateTreeInfo: treeInfo, // destination state tree
});
instructions.push(compressInstruction);
// Step 6: Sign and send transaction
const additionalSigners = dedupeSigner(payer, [owner]);
const { blockhash } = await connection.getLatestBlockhash();
const tx = buildAndSignTx(instructions, payer, blockhash, additionalSigners);
// For production: Add address lookup table to reduce transaction size and fees
// const lookupTableAddress = new PublicKey("9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ"); // mainnet // or "qAJZMgnQJ8G6vA3WRcjD9Jan1wtKkaCFWLWskxJrR5V" for devnet
// const lookupTableAccount = (await connection.getAddressLookupTable(lookupTableAddress)).value!;
// const tx = buildAndSignTx(instructions, payer, blockhash, additionalSigners, [lookupTableAccount]);
const txId = await sendAndConfirmTx(connection, tx);
console.log(`\nAirdrop completed!`);
console.log(`Transaction: ${txId}`);
// Step 7: Verify distribution via getCompressedTokenAccountsByOwner
for (let i = 0; i < airDropAddresses.length; i++) {
const recipientAccounts = await connection.getCompressedTokenAccountsByOwner(airDropAddresses[i], { mint });
const balance = recipientAccounts.items.reduce((sum, account) => sum + Number(account.parsed.amount), 0);
console.log(`Recipient ${i + 1} (${airDropAddresses[i].toString()}): ${balance / 1e9} compressed tokens`);
}
const finalSplBalance = await connection.getTokenAccountBalance(sourceTokenAccount.address);
console.log(`\nSender balance after airdrop: ${finalSplBalance.value.uiAmount} SPL tokens`);
return txId;
})();
```
For small airdrops (\<10,000 recipients). [View the source code here](https://github.com/Lightprotocol/example-token-distribution/blob/main/src/simple-airdrop/simple-airdrop.ts).
### Environment Setup
Configure your environment variables:
```bash .env theme={null}
PAYER_KEYPAIR=YOUR_BASE58_ENCODED_PRIVATE_KEY
MINT_ADDRESS=YOUR_MINT_ADDRESS
```
### Mint SPL Tokens
Use your existing mint or mint SPL tokens to your wallet for distribution.
```typescript expandable theme={null}
import { Rpc, createRpc } from '@lightprotocol/stateless.js';
import { createMint } from '@lightprotocol/compressed-token';
import {
getOrCreateAssociatedTokenAccount,
mintTo as mintToSpl,
} from '@solana/spl-token';
import { PAYER_KEYPAIR, RPC_ENDPOINT } from '../constants';
const payer = PAYER_KEYPAIR;
const connection: Rpc = createRpc(RPC_ENDPOINT);
const decimals = 9;
const mintAmount = 100;
(async () => {
// airdrop lamports to pay tx fees
// await confirmTx(
// connection,
// await connection.requestAirdrop(payer.publicKey, 1e7)
// );
const { mint, transactionSignature } = await createMint(
connection,
payer,
payer.publicKey,
decimals,
);
console.log(
`create-mint success! txId: ${transactionSignature}, mint: ${mint.toBase58()}`,
);
const ata = await getOrCreateAssociatedTokenAccount(
connection,
payer,
mint,
payer.publicKey,
);
console.log(`ata: ${ata.address}`);
const mintTxId = await mintToSpl(
connection,
payer,
mint,
ata.address,
payer.publicKey,
mintAmount,
);
console.log(`mint-spl success! txId: ${mintTxId}`);
})();
```
### Execute Airdrop
Run the airdrop script with your configured environment:
```typescript expandable theme={null}
// 1. Load environment and select compression infrastructure with getStateTreeInfos() and getTokenPoolInfos()
// 2. Build LightTokenProgram.compress() instruction for multiple recipients in one transaction
// 3. Execute transaction with compute budget, address lookup table, and confirm with sendAndConfirmTx()
// 4. Verify distribution via getCompressedTokenAccountsByOwner
import {
PublicKey,
TransactionInstruction,
ComputeBudgetProgram,
} from '@solana/web3.js';
import {
LightTokenProgram,
getTokenPoolInfos,
selectTokenPoolInfo,
} from '@lightprotocol/compressed-token';
import {
bn,
buildAndSignTx,
calculateComputeUnitPrice,
createRpc,
dedupeSigner,
Rpc,
selectStateTreeInfo,
sendAndConfirmTx,
} from '@lightprotocol/stateless.js';
import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token';
import { MINT_ADDRESS, PAYER_KEYPAIR, RPC_ENDPOINT } from '../constants';
(async () => {
const connection: Rpc = createRpc(RPC_ENDPOINT);
const mintAddress = MINT_ADDRESS;
const payer = PAYER_KEYPAIR;
const owner = payer;
const recipients = [
PublicKey.default,
// ...
];
// 1. Select a state tree
const treeInfos = await connection.getStateTreeInfos(); // Fixed: removed deprecated getCachedActiveStateTreeInfos
const treeInfo = selectStateTreeInfo(treeInfos);
// 2. Select SPL interface
const tokenPoolInfos = await getTokenPoolInfos(connection, mintAddress);
const tokenPoolInfo = selectTokenPoolInfo(tokenPoolInfos);
// Create an SPL token account for the sender.
// The sender will send tokens from this account to the recipients as compressed tokens.
const sourceTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
payer,
mintAddress,
payer.publicKey,
);
// 1 recipient = 120_000 CU
// 5 recipients = 170_000 CU
const units = 120_000;
const amount = bn(333);
// To land faster, replace this with a dynamic fee based on network
// conditions.
const microLamports = calculateComputeUnitPrice(20_000, units);
const instructions: TransactionInstruction[] = [
ComputeBudgetProgram.setComputeUnitLimit({ units }),
ComputeBudgetProgram.setComputeUnitPrice({
microLamports,
}),
];
const compressInstruction = await LightTokenProgram.compress({
payer: payer.publicKey,
owner: owner.publicKey,
source: sourceTokenAccount.address,
toAddress: recipients,
amount: recipients.map(() => amount),
mint: mintAddress,
outputStateTreeInfo: treeInfo,
tokenPoolInfo,
});
instructions.push(compressInstruction);
// https://www.zkcompression.com/developers/protocol-addresses-and-urls#lookup-tables
const lookupTableAddress = new PublicKey(
'9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ', // mainnet
// "qAJZMgnQJ8G6vA3WRcjD9Jan1wtKkaCFWLWskxJrR5V" // devnet
);
// Get the lookup table account state
const lookupTableAccount = (
await connection.getAddressLookupTable(lookupTableAddress)
).value!;
const additionalSigners = dedupeSigner(payer, [owner]);
const { blockhash } = await connection.getLatestBlockhash();
const tx = buildAndSignTx(
instructions,
payer,
blockhash,
additionalSigners,
[lookupTableAccount],
);
const txId = await sendAndConfirmTx(connection, tx);
console.log(`txId: ${txId}`);
})();
```
For large-scale airdrops (10,000+ recipients) we recommend batched instructions.\
[View the source code here.](https://github.com/Lightprotocol/example-token-distribution/tree/main/src/optimized-airdrop)
This implementation creates the mint and executes the airdrop in a single operation. You only need to configure your RPC endpoint and payer keypair.
### Create Instructions
Process recipients in chunks and create batched instructions with optimized compute limits.
```typescript create-instructions.ts expandable theme={null}
// 1. Process recipients in chunks with selectStateTreeInfo() and selectTokenPoolInfo() per batch
// 2. Create LightTokenProgram.compress() instructions with ComputeBudgetProgram limits for multiple recipients
// 3. Return batched instructions for optimized large-scale airdrop execution
import {
LightTokenProgram,
TokenPoolInfo,
} from "@lightprotocol/compressed-token";
import {
bn,
selectStateTreeInfo,
StateTreeInfo,
} from "@lightprotocol/stateless.js";
import {
ComputeBudgetProgram,
TransactionInstruction,
PublicKey,
} from "@solana/web3.js";
interface CreateAirdropInstructionsParams {
amount: number | bigint;
recipients: PublicKey[];
payer: PublicKey;
sourceTokenAccount: PublicKey;
mint: PublicKey;
stateTreeInfos: StateTreeInfo[];
tokenPoolInfos: TokenPoolInfo[];
maxRecipientsPerInstruction?: number;
maxInstructionsPerTransaction?: number;
computeUnitLimit?: number;
computeUnitPrice?: number | undefined;
}
export type InstructionBatch = TransactionInstruction[];
export async function createAirdropInstructions({
amount,
recipients,
payer,
sourceTokenAccount,
mint,
stateTreeInfos,
tokenPoolInfos,
maxRecipientsPerInstruction = 5,
maxInstructionsPerTransaction = 3,
computeUnitLimit = 500_000,
computeUnitPrice = undefined,
}: CreateAirdropInstructionsParams): Promise {
const instructionBatches: InstructionBatch[] = [];
const amountBn = bn(amount.toString());
// Process recipients in chunks
for (
let i = 0;
i < recipients.length;
i += maxRecipientsPerInstruction * maxInstructionsPerTransaction
) {
const instructions: TransactionInstruction[] = [];
instructions.push(
ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnitLimit })
);
if (computeUnitPrice) {
instructions.push(
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: computeUnitPrice,
})
);
}
const treeInfo = selectStateTreeInfo(stateTreeInfos);
const tokenPoolInfo = selectTokenPoolInfo(tokenPoolInfos);
for (let j = 0; j < maxInstructionsPerTransaction; j++) {
const startIdx = i + j * maxRecipientsPerInstruction;
const recipientBatch = recipients.slice(
startIdx,
startIdx + maxRecipientsPerInstruction
);
if (recipientBatch.length === 0) break;
const compressIx = await LightTokenProgram.compress({
payer,
owner: payer,
source: sourceTokenAccount,
toAddress: recipientBatch,
amount: recipientBatch.map(() => amountBn),
mint,
tokenPoolInfo,
outputStateTreeInfo: treeInfo,
});
instructions.push(compressIx);
}
if (
(computeUnitPrice && instructions.length > 2) ||
(!computeUnitPrice && instructions.length > 1)
) {
instructionBatches.push(instructions);
}
}
return instructionBatches;
}
```
### Update Blockhash
Maintain fresh blockhashes with background refresh loop using `getLatestBlockhash()` every 30 seconds.
```typescript update-blockhash.ts expandable theme={null}
import { Rpc } from "@lightprotocol/stateless.js";
// 1. Fetch initial blockhash with getLatestBlockhash() and store in exported variable
// 2. Set up background refresh loop with setTimeout() to update blockhash every 30 seconds
// 3. Provide AbortSignal support to stop background updates when airdrop completes
export let currentBlockhash: string;
export async function updateBlockhash(
connection: Rpc,
signal: AbortSignal
): Promise {
try {
const { blockhash } = await connection.getLatestBlockhash();
currentBlockhash = blockhash;
console.log(`Initial blockhash: ${currentBlockhash}`);
} catch (error) {
console.error("Failed to fetch initial blockhash:", error);
return;
}
// Update blockhash in the background
(function updateInBackground() {
if (signal.aborted) return;
const timeoutId = setTimeout(async () => {
if (signal.aborted) return;
try {
const { blockhash } = await connection.getLatestBlockhash();
currentBlockhash = blockhash;
console.log(`Updated blockhash: ${currentBlockhash}`);
} catch (error) {
console.error("Failed to update blockhash:", error);
}
updateInBackground();
}, 30_000);
signal.addEventListener("abort", () => clearTimeout(timeoutId));
})();
}
```
### Sign and Send
Execute batched transactions with `VersionedTransaction`, retry logic, and `sendAndConfirmTx()` confirmation.
```typescript sign-and-send.ts expandable theme={null}
// 1. Initialize blockhash updates with updateBlockhash() and get address lookup table with getAddressLookupTable()
// 2. Process instruction batches with VersionedTransaction and retry logic for failed transactions
// 3. Yield batch results with sendAndConfirmTx() confirmation and comprehensive error handling
import { Rpc, sendAndConfirmTx } from "@lightprotocol/stateless.js";
import {
Keypair,
PublicKey,
TransactionMessage,
VersionedTransaction,
} from "@solana/web3.js";
import { InstructionBatch } from "./create-instructions";
import { currentBlockhash, updateBlockhash } from "./update-blockhash";
import bs58 from "bs58";
export enum BatchResultType {
Success = "success",
Error = "error",
}
export type BatchResult =
| { type: BatchResultType.Success; index: number; signature: string }
| { type: BatchResultType.Error; index: number; error: string };
export async function* signAndSendAirdropBatches(
batches: InstructionBatch[],
payer: Keypair,
connection: Rpc,
maxRetries = 3
): AsyncGenerator {
const abortController = new AbortController();
const { signal } = abortController;
await updateBlockhash(connection, signal);
const statusMap = new Array(batches.length).fill(0); // Initialize all as pending (0)
// Use zk-compression look up table for your network
// https://www.zkcompression.com/developers/protocol-addresses-and-urls#lookup-tables
const lookupTableAddress = new PublicKey(
"9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ"
);
// Get the lookup table account
const lookupTableAccount = (
await connection.getAddressLookupTable(lookupTableAddress)
).value!;
while (statusMap.includes(0)) {
// Continue until all are confirmed or errored
const pendingBatches = statusMap.filter((status) => status === 0).length;
console.log(`Sending ${pendingBatches} transactions`);
const sends = statusMap.map(async (status, index) => {
if (status !== 0) return; // Skip non-pending batches
let retries = 0;
while (retries < maxRetries && statusMap[index] === 0) {
if (!currentBlockhash) {
console.warn("Waiting for blockhash to be set...");
await new Promise((resolve) => setTimeout(resolve, 1000));
continue;
}
try {
const tx = new VersionedTransaction(
new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: currentBlockhash,
instructions: batches[index],
}).compileToV0Message([lookupTableAccount])
);
tx.sign([payer]);
const sig = bs58.encode(tx.signatures[0]);
console.log(`Batch ${index} signature: ${sig}`);
const confirmedSig = await sendAndConfirmTx(connection, tx, {
skipPreflight: true,
commitment: "confirmed",
});
if (confirmedSig) {
statusMap[index] = 1; // Mark as confirmed
return {
type: BatchResultType.Success,
index,
signature: confirmedSig,
};
}
} catch (e) {
retries++;
console.warn(`Retrying batch ${index}, attempt ${retries + 1}`);
if (retries >= maxRetries) {
statusMap[index] = `err: ${(e as Error).message}`; // Mark as error
return {
type: BatchResultType.Error,
index,
error: (e as Error).message,
};
}
}
}
});
const results = await Promise.all(sends);
for (const result of results) {
if (result) yield result as BatchResult;
}
}
// Stop the blockhash update loop
abortController.abort();
}
```
### Main Airdrop File
Put it all together in the main airdrop file.
```typescript airdrop.ts expandable theme={null}
// 1. Create compressed mint with createMint(), mint supply with mintTo(), get infrastructure with getStateTreeInfos() and getTokenPoolInfos() (SPL interface infos)
// 2. Generate batched compression instructions with createAirdropInstructions() - create LightTokenProgram.compress() calls
// 3. Execute batched airdrop with signAndSendAirdropBatches() - sign transactions and confirm with sendAndConfirmTx() for large-scale distribution
import { Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
import {
calculateComputeUnitPrice,
createRpc,
Rpc,
} from "@lightprotocol/stateless.js";
import { createMint, getTokenPoolInfos } from "@lightprotocol/compressed-token";
import { getOrCreateAssociatedTokenAccount, mintTo } from "@solana/spl-token";
import { createAirdropInstructions } from "./create-instructions";
import { BatchResultType, signAndSendAirdropBatches } from "./sign-and-send";
import dotenv from "dotenv";
import bs58 from "bs58";
dotenv.config();
// Step 1: Setup environment and RPC connection
const RPC_ENDPOINT = `https://mainnet.helius-rpc.com?api-key=${process.env.HELIUS_API_KEY}`;
const connection: Rpc = createRpc(RPC_ENDPOINT);
const PAYER = Keypair.fromSecretKey(bs58.decode(process.env.PAYER_KEYPAIR!));
// Step 2: Define airdrop recipient list (20 example addresses)
const recipients = [
"GMPWaPPrCeZPse5kwSR3WUrqYAPrVZBSVwymqh7auNW7",
"GySGrTgPtPfMtYoYTmwUdUDFwVJbFMfip7QZdhgXp8dy",
"Bk1r2vcgX2uTzwV3AUyfRbSfGKktoQrQufBSrHzere74",
"8BvkadZ6ycFNmQF7S1MHRvEVNb1wvDBFdjkAUnxjK9Ug",
"EmxcvFKXsWLzUho8AhV9LCKeKRFHg5gAs4sKNJwhe5PF",
"6mqdHkSpcvNexmECjp5XLt9V9KnSQre9TvbMLGr6sEPM",
"3k4MViTWXBjFvoUZiJcNGPvzrqnTa41gcrbWCMMnV6ys",
"2k6BfYRUZQHquPtpkyJpUx3DzM7W3K6H95igtJk8ztpd",
"89jPyNNLCcqWn1RZThSS4jSqU5VCJkR5mAaSaVzuuqH4",
"3MzSRLf9jSt6d1MFFMMtPfUcDY6XziRxTB8C5mfvgxXG",
"9A1H6f3N8mpAPSdfqvYRD4cM1NwDZoMe3yF5DwibL2R2",
"PtUAhLvUsVcoesDacw198SsnMoFNVskR5pT3QvsBSQw",
"6C6W6WpgFK8TzTTMNCPMz2t9RaMs4XnkfB6jotrWWzYJ",
"8sLy9Jy8WSh6boq9xgDeBaTznn1wb1uFpyXphG3oNjL5",
"GTsQu2XCgkUczigdBFTWKrdDgNKLs885jKguyhkqdPgV",
"85UK4bjC71Jwpyn8mPSaW3oYyEAiHPbESByq9s5wLcke",
"9aEJT4CYHEUWwwSQwueZc9EUjhWSLD6AAbpVmmKDeP7H",
"CY8QjRio1zd9bYWMKiVRrDbwVenf3JzsGf5km5zLgY9n",
"CeHbdxgYifYhpB6sXGonKzmaejqEfq2ym5utTmB6XMVv",
"4z1qss12DjUzGUkK1fFesqrUwrEVJJvzPMNkwqYnbAR5",
].map((address) => new PublicKey(address));
(async () => {
// Step 3: Create compressed mint and register for compression
// 3a: Call createMint() to initialize mint with compression pool
const { mint, transactionSignature } = await createMint(
connection,
PAYER, // fee payer
PAYER.publicKey, // mint authority
9 // decimals
);
console.log(
`create-mint success! txId: ${transactionSignature}, mint: ${mint.toBase58()}`
);
// Step 4: Create associated token account for distributor
// 4a: Ensure PAYER has ATA for holding tokens before compression
const ata = await getOrCreateAssociatedTokenAccount(
connection,
PAYER, // fee payer
mint, // token mint
PAYER.publicKey // token owner
);
console.log(`ATA: ${ata.address.toBase58()}`);
// Step 5: Mint initial token supply to distributor
// 5a: Create 10 billion tokens in the ATA for airdrop distribution
const mintToTxId = await mintTo(
connection,
PAYER, // fee payer and mint authority
mint, // token mint
ata.address, // destination ATA
PAYER.publicKey, // mint authority
10e9 * LAMPORTS_PER_SOL // amount: 10 billion tokens with decimals
);
console.log(`mint-to success! txId: ${mintToTxId}`);
// Step 6: Get compression infrastructure for batch operations
// 6a: Fetch available state trees for compressed account storage
const stateTreeInfos = await connection.getStateTreeInfos();
// 6b: Get SPL interface infos for compression
const tokenPoolInfos = await getTokenPoolInfos(connection, mint);
// Step 7: Create instruction batches for large-scale airdrop
// 7a: Generate batched compression instructions with compute optimization
const instructionBatches = await createAirdropInstructions({
amount: 1e6, // 1 million tokens per recipient
recipients, // array of recipient addresses
payer: PAYER.publicKey, // transaction fee payer
sourceTokenAccount: ata.address, // source ATA holding SPL tokens
mint, // token mint
stateTreeInfos, // state trees for compressed accounts
tokenPoolInfos,
computeUnitPrice: calculateComputeUnitPrice(10_000, 500_000), // dynamic priority fee
});
// Step 8: Execute batched airdrop with error handling
// 8a: Process instruction batches with retry logic and confirmation
for await (const result of signAndSendAirdropBatches(
instructionBatches,
PAYER,
connection
)) {
if (result.type === BatchResultType.Success) {
console.log(`Batch ${result.index} confirmed: ${result.signature}`);
} else if (result.type === BatchResultType.Error) {
console.log(`Batch ${result.index} failed: ${result.error}`);
// Use result.index to access the specific batch in instructionBatches
const failedBatch = instructionBatches[result.index];
console.log(`Failed batch instructions:`, failedBatch);
// Additional logic to handle failed instructions
}
}
console.log("Airdrop process complete.");
})();
```
```text theme={null}
---
description: Distribute compressed tokens via airdrop
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Distribute compressed tokens via airdrop
Context:
- Guide: https://zkcompression.com/compressed-tokens/token-distribution
- Skills and resources index: https://zkcompression.com/skill.md
- Dedicated skill: https://github.com/Lightprotocol/skills/tree/main/skills/airdrop
- Packages: @lightprotocol/compressed-token, @lightprotocol/stateless.js, @solana/spl-token
- Example repo: https://github.com/Lightprotocol/example-token-distribution
- Webapp alternative: https://airship.helius.dev/ (Airship by Helius Labs, up to 200k recipients)
Key APIs: LightTokenProgram.compress(), getTokenPoolInfos(), selectTokenPoolInfo(), getStateTreeInfos(), selectStateTreeInfo(), buildAndSignTx(), sendAndConfirmTx()
### 1. Index project
- Grep `LightTokenProgram|compress|getTokenPoolInfos|selectTokenPoolInfo|getStateTreeInfos|@lightprotocol|airdrop|distribution` across src/
- Glob `**/*.ts` for project structure
- Identify: existing airdrop/distribution logic, token minting setup, recipient list format
- Check package.json for existing @lightprotocol/* or @solana/spl-token dependencies
- Task subagent (Grep/Read/WebFetch) if project has multiple packages to scan in parallel
### 2. Read references
- WebFetch the guide above — review all three tabs (Localnet Guide, Simple Airdrop, Batched)
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: what scale? (<10k recipients = simple airdrop, 10k+ = batched)
- AskUserQuestion: localnet testing first, or production deploy?
- AskUserQuestion: do you have an existing SPL mint, or need to create one?
- AskUserQuestion: do you need decompression/claim functionality, or just direct distribution?
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan
- For simple airdrop: create mint → mint SPL tokens → LightTokenProgram.compress() with recipients array
- For batched: create instruction batches → manage blockhash refresh → sign and send with retry logic
- Address lookup table needed for production (mainnet: 9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ)
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- Add deps if missing: Bash `npm install @lightprotocol/compressed-token @lightprotocol/stateless.js @solana/spl-token`
- Set up RPC: `createRpc(RPC_ENDPOINT)` with a ZK Compression endpoint (Helius, Triton)
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- Bash `tsc --noEmit`
- Bash run existing test suite or execute localnet test airdrop
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work
```
# Advanced Features
Solana Wallets like Phantom and Backpack already support compressed tokens.
Still, you can let users decompress to SPL via your Frontend to customize claims.
Add decompression of SPL Tokens with this script.
```typescript expandable theme={null}
import {
bn,
buildAndSignTx,
sendAndConfirmTx,
dedupeSigner,
Rpc,
createRpc,
} from "@lightprotocol/stateless.js";
import { ComputeBudgetProgram, Keypair, PublicKey } from "@solana/web3.js";
import {
LightTokenProgram,
getTokenPoolInfos,
selectMinCompressedTokenAccountsForTransfer,
selectTokenPoolInfosForDecompression,
} from "@lightprotocol/compressed-token";
import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token";
import bs58 from "bs58";
import dotenv from "dotenv";
dotenv.config();
// Set these values in your .env file
const RPC_ENDPOINT = process.env.RPC_ENDPOINT;
const mint = new PublicKey(process.env.MINT_ADDRESS!);
const payer = Keypair.fromSecretKey(bs58.decode(process.env.PAYER_KEYPAIR!));
const owner = payer;
const amount = 1e5;
const connection: Rpc = createRpc(RPC_ENDPOINT);
(async () => {
// 1. Create an associated token account for the user if it doesn't exist
const ata = await getOrCreateAssociatedTokenAccount(
connection,
payer,
mint,
payer.publicKey
);
// 2. Fetch compressed token accounts
const compressedTokenAccounts =
await connection.getCompressedTokenAccountsByOwner(owner.publicKey, {
mint,
});
// 3. Select
const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer(
compressedTokenAccounts.items,
bn(amount)
);
// 4. Fetch validity proof
const proof = await connection.getValidityProof(
inputAccounts.map((account) => account.compressedAccount.hash)
);
// 5. Fetch SPL interface infos
const tokenPoolInfos = await getTokenPoolInfos(connection, mint);
// 6. Select
const selectedTokenPoolInfos = selectTokenPoolInfosForDecompression(
tokenPoolInfos,
amount
);
// 7. Build instruction
const ix = await LightTokenProgram.decompress({
payer: payer.publicKey,
inputCompressedTokenAccounts: inputAccounts,
toAddress: ata.address,
amount,
tokenPoolInfos: selectedTokenPoolInfos,
recentInputStateRootIndices: proof.rootIndices,
recentValidityProof: proof.compressedProof,
});
// 8. Sign, send, and confirm
const { blockhash } = await connection.getLatestBlockhash();
const additionalSigners = dedupeSigner(payer, [owner]);
const signedTx = buildAndSignTx(
[ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }), ix],
payer,
blockhash,
additionalSigners
);
return await sendAndConfirmTx(connection, signedTx);
})();
```
Set priority fees dynamically for decompression. Learn more [here](https://docs.helius.dev/guides/sending-transactions-on-solana#summary).
### Native Swap via Jup-API
If you have a custom FE, you can let users swap compressed tokens using the Jup-API. A reference implementation is available [here](https://github.com/Lightprotocol/example-jupiter-swap-node).
## Next Steps
Customize token distribution and let users claim.
| | distributor | simple-claim |
| -------------- | -------------- | --------------- |
| Vesting | Linear Vesting | Cliff at Slot X |
| Partial claims | Yes | No |
| Clawback | Yes | No |
| Frontend | REST API + CLI | None |
The programs are reference implementations and should be audited before production use.
Merkle distributor based on jito airdrop and optimized with rent-free PDAs.
Simple SPL token airdrop with claim.
# Add Wallet Support for Compressed Tokens
Source: https://www.zkcompression.com/compressed-tokens/wallet-integration
Guide to add Compressed Token Support to Your Wallet Application
Leading Solana Wallets like Phantom and Backpack already support compressed tokens.
# Integration Steps
### Display Compressed Token Balances
```javascript theme={null}
import { Rpc, createRpc } from '@lightprotocol/stateless.js';
import { PublicKey } from '@solana/web3.js';
const connection: Rpc = createRpc();
const publicKey = new PublicKey('FWwR2s4TwpWN3nkCzVfhuPrpePG8kNzBXAxEbNsaDFNu');
(async () => {
const balances = await connection.getCompressedTokenBalancesByOwnerV2(publicKey);
if (balances.value.items.length === 0) {
console.log("No compressed token balances found");
return;
}
for (const item of balances.value.items) {
const balanceValue = typeof item.balance === 'string'
? parseInt(item.balance, 16)
: item.balance;
const mintInfo = await connection.getAccountInfo(new PublicKey(item.mint));
const decimals = mintInfo.data[44];
const formattedBalance = balanceValue / Math.pow(10, decimals);
console.log(`Mint: ${item.mint}`);
console.log(`Balance: ${formattedBalance} tokens\n`);
}
})();
```
### Get Transaction History
```typescript theme={null}
import { Rpc, createRpc } from '@lightprotocol/stateless.js';
import { PublicKey } from '@solana/web3.js';
const connection: Rpc = createRpc();
const publicKey = new PublicKey('FWwR2s4TwpWN3nkCzVfhuPrpePG8kNzBXAxEbNsaDFNu');
(async () => {
const signatures = await connection.getCompressionSignaturesForTokenOwner(publicKey);
if (signatures.items.length > 0) {
console.log(`Signatures:`);
signatures.items.forEach((sig, index) => {
console.log(`${index + 1}. ${sig.signature}`);
console.log(` Slot: ${sig.slot}`);
console.log(` Time: ${new Date(sig.blockTime * 1000).toISOString()}`);
});
} else {
console.log("No transactions found");
}
})();
```
```typescript theme={null}
import { Rpc, createRpc } from '@lightprotocol/stateless.js';
import { PublicKey } from '@solana/web3.js';
const RPC_ENDPOINT = process.env.RPC_ENDPOINT || 'https://devnet.helius-rpc.com?api-key=';
const connection: Rpc = createRpc(RPC_ENDPOINT, RPC_ENDPOINT);
const publicKey = new PublicKey('FWwR2s4TwpWN3nkCzVfhuPrpePG8kNzBXAxEbNsaDFNu');
(async () => {
try {
const signatures = await connection.getCompressionSignaturesForTokenOwner(publicKey);
if (signatures.items.length === 0) {
console.log("No transactions found");
return;
}
console.log(`Latest Transaction:`);
console.log(`Signature: ${signatures.items[0].signature}`);
console.log(`Slot: ${signatures.items[0].slot}`);
const txInfo = await connection.getTransactionWithCompressionInfo(signatures.items[0].signature);
if (!txInfo) {
console.log('Transaction not found or has no compression info');
return;
}
if (txInfo.compressionInfo) {
console.log(`\nClosed Accounts: ${txInfo.compressionInfo.closedAccounts.length}`);
console.log(`Opened Accounts: ${txInfo.compressionInfo.openedAccounts.length}`);
}
} catch (error) {
console.error('Error fetching transaction history:', error);
}
})();
```
### Send Compressed Tokens
```typescript expandable theme={null}
import {
Rpc,
createRpc,
bn,
dedupeSigner,
sendAndConfirmTx,
buildAndSignTx,
} from "@lightprotocol/stateless.js";
import {
CompressedTokenProgram,
selectMinCompressedTokenAccountsForTransfer,
} from "@lightprotocol/compressed-token";
import { ComputeBudgetProgram, Keypair, PublicKey } from "@solana/web3.js";
const connection: Rpc = createRpc();
const mint = new PublicKey("MINT_ADDRESS");
const payer = PAYER_KEYPAIR;
const owner = payer;
const recipient = Keypair.generate();
const amount = bn(1e8);
(async () => {
const compressedTokenAccounts =
await connection.getCompressedTokenAccountsByOwner(owner.publicKey, { mint });
if (compressedTokenAccounts.items.length === 0) {
console.log("No compressed token accounts found");
return;
}
const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer(
compressedTokenAccounts.items,
amount
);
const proof = await connection.getValidityProof(
inputAccounts.map((account) => account.compressedAccount.hash)
);
const ix = await CompressedTokenProgram.transfer({
payer: payer.publicKey,
inputCompressedTokenAccounts: inputAccounts,
toAddress: recipient.publicKey,
amount,
recentInputStateRootIndices: proof.rootIndices,
recentValidityProof: proof.compressedProof,
});
const { blockhash } = await connection.getLatestBlockhash();
const additionalSigners = dedupeSigner(payer, [owner]);
const signedTx = buildAndSignTx(
[ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }), ix],
payer,
blockhash,
additionalSigners
);
const transferTxId = await sendAndConfirmTx(connection, signedTx);
console.log(`Transaction: ${transferTxId}`);
})();
```
### Prerequisites
Make sure you have dependencies and developer environment set up!
**Dependencies**
```bash theme={null}
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
```bash theme={null}
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
```bash theme={null}
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/compressed-token@^0.23.0
```
```bash theme={null}
# npm
npm install @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# yarn
yarn add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
# pnpm
pnpm add @lightprotocol/stateless.js@^0.23.0 \
@lightprotocol/token-interface@^0.1.2
```
**Developer Environment**
By default, all guides use Localnet.
```bash theme={null}
npm install -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
yarn global add @lightprotocol/zk-compression-cli
```
```bash theme={null}
pnpm add -g @lightprotocol/zk-compression-cli
```
```bash theme={null}
# Start a local test validator
light test-validator
## ensure you have the Solana CLI accessible in your system PATH
```
```typescript theme={null}
// createRpc() defaults to local test validator endpoints
import {
Rpc,
createRpc,
} from "@lightprotocol/stateless.js";
const connection: Rpc = createRpc();
async function main() {
let slot = await connection.getSlot();
console.log(slot);
let health = await connection.getIndexerHealth(slot);
console.log(health);
// "Ok"
}
main();
```
Replace `` with your actual API key. [Get your API key here](https://www.helius.dev/zk-compression), if you don't have one yet.
```typescript theme={null}
import { createRpc } from "@lightprotocol/stateless.js";
const RPC_ENDPOINT = "https://devnet.helius-rpc.com?api-key=";
const connection = createRpc(RPC_ENDPOINT);
async function main() {
let slot = await connection.getSlot();
console.log(slot);
let health = await connection.getIndexerHealth(slot);
console.log(health);
// "Ok"
}
main();
```
### Display Compressed Token Balances
This example fetches and displays all compressed token balances for a wallet address.
```javascript theme={null}
import { Rpc, createRpc } from '@lightprotocol/stateless.js';
import { Keypair, PublicKey } from '@solana/web3.js';
import * as fs from 'fs';
import * as os from 'os';
const connection: Rpc = createRpc();
const walletPath = `${os.homedir()}/.config/solana/id.json`;
const secretKey = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
const payer = Keypair.fromSecretKey(Buffer.from(secretKey));
(async () => {
const balances = await connection.getCompressedTokenBalancesByOwnerV2(payer.publicKey);
if (balances.value.items.length === 0) {
console.log("No compressed token balances found");
return;
}
for (const item of balances.value.items) {
const balanceValue = typeof item.balance === 'string'
? parseInt(item.balance, 16)
: item.balance;
const mintInfo = await connection.getAccountInfo(new PublicKey(item.mint));
const decimals = mintInfo.data[44];
const formattedBalance = balanceValue / Math.pow(10, decimals);
console.log(`Mint: ${item.mint}`);
console.log(`Balance: ${formattedBalance} tokens\n`);
}
})();
```
### Get Transaction History
This example retrieves compression transaction signatures to display transaction history of the wallet.
```javascript theme={null}
import { Rpc, createRpc } from '@lightprotocol/stateless.js';
import { Keypair } from '@solana/web3.js';
import * as fs from 'fs';
import * as os from 'os';
const connection: Rpc = createRpc();
const walletPath = `${os.homedir()}/.config/solana/id.json`;
const secretKey = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
const payer = Keypair.fromSecretKey(Buffer.from(secretKey));
(async () => {
const signatures = await connection.getCompressionSignaturesForTokenOwner(payer.publicKey);
if (signatures.items.length > 0) {
console.log(`Signatures:`);
signatures.items.forEach((sig, index) => {
console.log(`${index + 1}. ${sig.signature}`);
console.log(` Slot: ${sig.slot}`);
console.log(` Time: ${new Date(sig.blockTime * 1000).toISOString()}`);
});
} else {
console.log("No transactions found");
}
})();
```
### Send Compressed Tokens
First, set up a test mint to and mint 10 compressed tokens to your filesystem wallet.
```typescript theme={null}
import { Keypair } from "@solana/web3.js";
import { Rpc, confirmTx, createRpc } from '@lightprotocol/stateless.js';
import { createMint, mintTo } from '@lightprotocol/compressed-token';
import * as fs from 'fs';
import * as os from 'os';
// 1. Setup RPC connection and load filesystem wallet for mint operations
// 2. Call createMint() to create SPL mint with interface PDA for compression
// 3. Call mintTo() to mint compressed tokens to filesystem wallet
const connection: Rpc = createRpc(); // defaults to localhost:8899
// Load wallet from filesystem
const walletPath = `${os.homedir()}/.config/solana/id.json`;
const secretKey = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
const payer = Keypair.fromSecretKey(Buffer.from(secretKey));
const mintKeypair = Keypair.generate();
(async() => {
// Fund payer with SOL
await connection.requestAirdrop(payer.publicKey, 1e9);
await new Promise(resolve => setTimeout(resolve, 1000));
// Create SPL mint with interface PDA for compression
const { mint, transactionSignature } = await createMint(
connection,
payer,
payer.publicKey, // mint authority
9,
mintKeypair,
);
console.log(`Mint address: ${mint.toString()}`);
console.log(`Transaction: ${transactionSignature}`);
// Mint compressed tokens to payer
const mintToTxId = await mintTo(
connection,
payer,
mint, // SPL mint with interface PDA for compression
payer.publicKey, // recipient address
payer,
10e9,
);
console.log(`\nMinted ${10e9 / 1e9} compressed token`);
console.log(`Transaction: ${mintToTxId}`);
})();
```
Make sure you add your Mint address to `send-tokens.ts`.
```typescript send-tokens.ts expandable theme={null}
// Compressed Token Transfer - Local
// 1. Load wallet and fetch compressed token accounts with getCompressedTokenAccountsByOwner()
// 2. Select accounts for transfer using selectMinCompressedTokenAccountsForTransfer()
// and get validity proof with getValidityProof()
// 3. Create transfer instruction with CompressedTokenProgram.transfer()
// and submit transaction with sendAndConfirmTx()
// 4. Verify balances via getCompressedTokenAccountsByOwner()
import {
Rpc,
createRpc,
bn,
dedupeSigner,
sendAndConfirmTx,
buildAndSignTx,
} from "@lightprotocol/stateless.js";
import {
CompressedTokenProgram,
selectMinCompressedTokenAccountsForTransfer,
} from "@lightprotocol/compressed-token";
import { ComputeBudgetProgram, Keypair, PublicKey } from "@solana/web3.js";
import * as fs from 'fs';
import * as os from 'os';
// Step 1: Setup RPC connection and define transfer parameters
const connection: Rpc = createRpc(); // defaults to localhost:8899
const mint = new PublicKey("MINT ADDRESS"); // Replace with mint address
// Load wallet from filesystem
const walletPath = `${os.homedir()}/.config/solana/id.json`;
const secretKey = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
const payer = Keypair.fromSecretKey(Buffer.from(secretKey));
const owner = payer;
const recipient = Keypair.generate();
const amount = bn(1e8);
(async () => {
// Step 2: Fetch compressed account hashes from state trees
const compressedTokenAccounts =
await connection.getCompressedTokenAccountsByOwner(owner.publicKey, {
mint, // SPL mint with interface PDA for compression
});
if (compressedTokenAccounts.items.length === 0) {
console.log("No compressed token accounts found for this mint");
return;
}
// Show initial sender balance
const initialBalance = compressedTokenAccounts.items.reduce((sum, account) => sum + Number(account.parsed.amount), 0);
console.log(`Sender balance: ${initialBalance / 1e8} compressed tokens`);
// Step 3: Select minimum compressed accounts for transfer amount
const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer(
compressedTokenAccounts.items,
amount
);
// Get validity proof for Merkle tree verification
const proof = await connection.getValidityProof(
inputAccounts.map((account) => account.compressedAccount.hash)
);
// Step 4: Create transfer instruction that consumes input accounts and creates new output accounts
const ix = await CompressedTokenProgram.transfer({
payer: payer.publicKey,
inputCompressedTokenAccounts: inputAccounts, // accounts to consume
toAddress: recipient.publicKey,
amount,
recentInputStateRootIndices: proof.rootIndices,
recentValidityProof: proof.compressedProof,
});
// Step 5: Build, sign, and submit transaction
const { blockhash } = await connection.getLatestBlockhash();
const additionalSigners = dedupeSigner(payer, [owner]);
const signedTx = buildAndSignTx(
[ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }), ix],
payer,
blockhash,
additionalSigners
);
const transferTxId = await sendAndConfirmTx(connection, signedTx);
console.log(`\nTransferred ${amount.toNumber() / 1e8} compressed tokens`);
console.log(`Transaction: ${transferTxId}`);
console.log(`Recipient: ${recipient.publicKey.toString()}`);
// Step 6: Verify via getCompressedTokenAccountsByOwner
const senderCompressedAccounts = await connection.getCompressedTokenAccountsByOwner(payer.publicKey, { mint });
const senderBalance = senderCompressedAccounts.items.reduce((sum, account) => sum + Number(account.parsed.amount), 0);
const recipientCompressedAccounts = await connection.getCompressedTokenAccountsByOwner(recipient.publicKey, { mint });
const recipientBalance = recipientCompressedAccounts.items.reduce((sum, account) => sum + Number(account.parsed.amount), 0);
console.log(`\nSummary compressed token balances:`);
console.log(`Sender balance: ${senderBalance / 1e8} compressed tokens`);
console.log(`Recipient balance: ${recipientBalance / 1e8} compressed token`);
return transferTxId;
})();
```
# Advanced Integrations
Use these integrations to let users convert between regular and compressed format as needed.
This example converts compressed tokens to regular SPL format using `CompressedTokenProgram.decompress().`
```javascript theme={null}
import {
bn,
buildAndSignTx,
sendAndConfirmTx,
dedupeSigner,
Rpc,
createRpc,
} from "@lightprotocol/stateless.js";
import { ComputeBudgetProgram } from "@solana/web3.js";
import {
CompressedTokenProgram,
getTokenPoolInfos,
selectMinCompressedTokenAccountsForTransfer,
selectTokenPoolInfosForDecompression,
} from "@lightprotocol/compressed-token";
// 1. Setup RPC connection and fetch compressed token accounts with getCompressedTokenAccountsByOwner()
// 2. Select accounts and token pool infos using selectMinCompressedTokenAccountsForTransfer() and selectTokenPoolInfosForDecompression()
// 3. Create decompress instruction with CompressedTokenProgram.decompress() and submit transaction
// Step 1: Setup RPC connection and define decompression parameters
const connection: Rpc = createRpc("https://mainnet.helius-rpc.com?api-key=";);
const payer = PAYER_KEYPAIR;
const owner = PAYER_KEYPAIR;
const mint = MINT_ADDRESS;
const amount = 1e5; // 100K tokens to decompress
(async () => {
// 1. Fetch compressed token accounts
const compressedTokenAccounts =
await connection.getCompressedTokenAccountsByOwner(owner.publicKey, {
mint,
});
// 2. Select
const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer(
compressedTokenAccounts.items,
bn(amount)
);
// 3. Fetch validity proof
const proof = await connection.getValidityProof(
inputAccounts.map((account) => account.compressedAccount.hash)
);
// 4. Fetch & Select tokenPoolInfos
const tokenPoolInfos = await getTokenPoolInfos(connection, mint);
const selectedTokenPoolInfos = selectTokenPoolInfosForDecompression(
tokenPoolInfos,
amount
);
// 5. Build instruction
const ix = await CompressedTokenProgram.decompress({
payer: payer.publicKey,
inputCompressedTokenAccounts: inputAccounts,
toAddress: owner.publicKey,
amount,
tokenPoolInfos: selectedTokenPoolInfos,
recentInputStateRootIndices: proof.rootIndices,
recentValidityProof: proof.compressedProof,
});
// 6. Sign, send, and confirm.
// Example with keypair:
const { blockhash } = await connection.getLatestBlockhash();
const additionalSigners = dedupeSigner(payer, [owner]);
const signedTx = buildAndSignTx(
[ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }), ix],
payer,
blockhash,
additionalSigners
);
return await sendAndConfirmTx(connection, signedTx);
})();
```
This example converts regular SPL tokens to compressed format using `CompressedTokenProgram.compress().`
```typescript theme={null}
// 1. Setup RPC connection and get user ATA with getOrCreateAssociatedTokenAccount()
// 2. Fetch state tree and token pool infos using getStateTreeInfos() and getTokenPoolInfos()
// 3. Create compress instruction with CompressedTokenProgram.compress() and submit transaction
import {
buildAndSignTx,
sendAndConfirmTx,
Rpc,
createRpc,
selectStateTreeInfo,
} from "@lightprotocol/stateless.js";
import { ComputeBudgetProgram } from "@solana/web3.js";
import {
CompressedTokenProgram,
getTokenPoolInfos,
selectTokenPoolInfo,
} from "@lightprotocol/compressed-token";
import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token";
// Step 1: Setup RPC connection and define compression parameters
const connection: Rpc = createRpc(
"https://mainnet.helius-rpc.com?api-key="
);
const payer = ;
const mint = ;
const amount = 1e5; // 100K tokens to compress
(async () => {
// Step 2: Get or create associated token account for SPL tokens
const sourceTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
payer, // fee payer
mint, // token mint address
payer.publicKey // token account owner
);
// Step 3: Fetch and select state tree info for compression
const treeInfos = await connection.getStateTreeInfos();
const treeInfo = selectStateTreeInfo(treeInfos);
// Step 4: Fetch and select interface PDA info for compression
const tokenPoolInfos = await getTokenPoolInfos(connection, mint);
const tokenPoolInfo = selectTokenPoolInfo(tokenPoolInfos);
// Step 5: Create compress instruction - transfer SPL tokens to pool and create compressed accounts
const compressInstruction = await CompressedTokenProgram.compress({
payer: payer.publicKey, // fee payer
owner: payer.publicKey, // owner of source SPL tokens
source: sourceTokenAccount.address, // source ATA address
toAddress: payer.publicKey, // recipient of compressed tokens (self)
amount, // amount to compress
mint, // token mint address
outputStateTreeInfo: treeInfo, // state tree for compressed accounts
tokenPoolInfo, // token pool for compression
});
// Step 6: Build, sign, and submit compression transaction
const { blockhash } = await connection.getLatestBlockhash();
const tx = buildAndSignTx(
[
ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }),
compressInstruction,
],
payer, // transaction signer
blockhash,
[payer] // additional signers
);
await sendAndConfirmTx(connection, tx);
})();
```
# Common Errors
If `getCompressedTokenBalancesByOwnerV2` returns empty:
* Ensure the wallet has compressed tokens (not regular SPL tokens)
* Verify you're on the correct network (devnet/mainnet)
# Next Steps
# Welcome to ZK Compression
Source: https://www.zkcompression.com/home
ZK Compression is a Solana framework that reduces the cost of token accounts and
PDAs by 99%.
## Compressed Token
| Creation | Solana | Compressed |
| :---------------- | -------------------: | -----------------: |
| **Token Account** | \~2,000,000 lamports | **5,000** lamports |
Rent-free token accounts that reduce account creation cost by 99%. Compressed tokens are supported by leading wallets like Phantom and Backpack, and work with any SPL or Token 2022 mint.
### Integration Guides
Integrate compressed tokens with Privy embedded wallets for rent-free token accounts.
Distribute SPL tokens to many recipients without paying upfront rent per account.
Display balances, fetch history, and send compressed tokens from your wallet application.
Use Token 2022 mints and extensions with compression.
### Cookbook & Reference
Step-by-step recipes for every compressed token operation.
TypeScript and Rust SDKs, Anchor constraints and JSON RPC methods.
## Compressed PDAs
| Creation Cost | Light / Compressed | Solana Default |
| :--------------- | -----------------: | -------------: |
| **100-byte PDA** | \~0.000015 SOL | 0.0016 SOL |
Compressed PDAs are the core primitive. They let you store user, app, or other infrequently accessed state without paying rent-exemption fees.
### Guides
Build and send transactions that read and write compressed PDA state from a client.
Create, update, close, reinitialize, and burn compressed accounts in your program.
Issue attestations backed by compressed accounts on devnet.
Prevent an instruction from running twice, rent-free. Useful for payments and ZK apps.
## AI Native Development
Install orchestrator agent skill or view [skill.md](https://www.zkcompression.com/skill.md):
```bash theme={null}
npx skills add https://zkcompression.com
```
Install or view [dedicated agent skills](/ai-tools/overview#agent-skills).
```
npx skills add Lightprotocol/skills
```
Connect AI tools to the Light Protocol repository via MCP and DeepWiki.
```bash theme={null}
claude mcp add -s user -t http deepwiki https://mcp.deepwiki.com/mcp
claude mcp add -s user -t http zkcompression https://www.zkcompression.com/mcp
```
```bash theme={null}
codex mcp add zkcompression -- npx -y mcp-remote@latest https://www.zkcompression.com/mcp
codex mcp add deepwiki -- npx -y mcp-remote@latest https://mcp.deepwiki.com/mcp
```
```json theme={null}
{
"mcpServers": {
"deepwiki": {
"serverUrl": "https://mcp.deepwiki.com/sse"
},
"zkcompression": {
"command": "npx",
"args": ["-y", "mcp-remote@latest", "https://www.zkcompression.com/mcp"]
}
}
}
```
Copy-paste prompts for common integration tasks.
## Resources
Learn about compressed tokens and ZK Compression Core.
View security audits and formal verification.
# Compressed Account Model
Source: https://www.zkcompression.com/learn/compressed-account-model
Overview to compressed accounts and comparison to Solana accounts.
# Overview
This guide assumes that you're familiar with [Solana's account
model](https://solana.com/docs/core/accounts).
Compressed accounts store state and are similar to regular Solana accounts but with five main differences:
* Each compressed account can be identified by its hash
* Each write to a compressed account changes its hash
* An `address` can optionally be set as a permanent unique ID of the compressed account.
* All compressed accounts are stored in sparse state trees. Only the tree's state root (i.e., a small fingerprint of all compressed accounts) is stored in the on-chain account space.
These differences make compressed accounts rent-free and allow the protocol to store state as calldata on the Solana ledger instead of the costly on-chain account space.
Compressed PDAs are compressed accounts with an address.
Compressed Token accounts are compressed accounts that don't need an address.
# In a Nutshell
Transactions can use compressed account data inside Solana's virtual machine as if it were stored on-chain by combining state compression and zero-knowledge proofs:
1. Millions of compressed accounts are stored as hashes in Merkle tree leaves
2. All accounts stored in a Merkle tree are compressed into a single root hash
3. The root hash is stored in one Solana account for cryptographic verification
4. Compressed account state is recorded on the Solana ledger
5. The latest compressed account state is fetched from your RPC provider
6. Compressed account state is verified against the on-chain root hash with a validity proof
* Merkle trees are provided by the protocol and Indexers generate validity proofs.
* Developers don't configure state Merkle trees or generate validity proofs.
[You will learn more about Merkle trees and validity proofs in the next section](/learn/merkle-trees-validity-proofs).
# Compressed Account Structure
Compressed accounts include the core Solana account fields (owner, lamports, data) plus additional fields to index and store compressed state.
```rust theme={null}
pub struct CompressedAccount {
pub address: Option<[u8; 32]>, // Optional persistent identifier
pub data: Option, // Account data
pub hash: [u8; 32], // Unique account hash
pub lamports: u64, // Account balance
pub owner: Pubkey, // Program that owns this account
}
```
Find the [source code
here](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-libs/client/src/indexer/types.rs#L508-L520).
```rust theme={null}
pub struct Account {
pub lamports: u64, // Account balance
pub data: Vec, // Arbitrary data (program state)
pub owner: Pubkey, // Program that owns this account
pub executable: bool, // Is this a program account
pub rent_epoch: Epoch, // Legacy field
}
```
Find the source code here:
[agave/sdk/account/src/lib.rs:48-60](https://github.com/anza-xyz/agave/blob/67412607f511ded3770031280b6aaf10607713fc/sdk/account/src/lib.rs#L48-L60)
# Address and Hash
Each compressed account can be **identified by its hash**, regardless of whether it has an address. By definition, whenever any data of a compressed account changes, its **hash changes**.
An **address** can serve as **optional** and additional **persistent identifier**.
* It's represented as 32 bytes in the format of a `PublicKey`.
* Addresses are optional, since ensuring that the address of a new account is unique incurs additional computational overhead.
Create an address wherever the state must be unique (such as for NFTs or certain PDAs) and requires a persistent identifier.
You don't need to create an address for fungible state (e.g., fungible tokens)
**Compressed Account Address Derivation**
Compressed account addresses are derived similar to PDAs.
* Like PDAs, compressed account addresses don't belong to a private key; rather, they're derived from the program that owns them.
* The key difference to regular PDAs is that compressed accounts require an **address tree** parameter.
* Address Merkle tree's store addresses of compressed accounts and ensure its uniqueness.
```typescript theme={null}
const seed = deriveAddressSeedV2([customSeed, signer.publicKey.toBytes()]);
const address = deriveAddressV2(seed, addressTree, new web3.PublicKey(programId));
```
Learn more about address derivation for a [Typescript Client
here](https://github.com/Lightprotocol/developer-content/blob/main/zk-compression-docs/compressed-pdas/client-library/typescript.md#derive-address).
```rust theme={null}
let (address, _) = derive_address(
&[b"custom_seed", keypair.pubkey().as_ref()],
&address_tree_info.tree,
&your_program::ID,
);
```
Learn more about address derivation for a [Rust Client
here](https://github.com/Lightprotocol/developer-content/blob/main/zk-compression-docs/compressed-pdas/client-library/rust.md#derive-address).
```typescript derive-pda.ts theme={null}
const [pda, bump] = PublicKey.findProgramAddressSync(
[Buffer.from("seed_string"), publicKey.toBuffer()],
programId
);
```
```rust derive-pda.rs theme={null}
let (pda, bump) = Pubkey::find_program_address(
&[
b"seed_string",
pubkey.as_ref()
],
&program_id
);
```
# Data Field
The `data` field contains the compressed account's program state:
```rust theme={null}
pub struct CompressedAccountData {
pub discriminator: [u8; 8], // Type identifier for account data
pub data: Vec, // Serialized program state
pub data_hash: [u8; 32], // Hash of the data field
}
```
* `discriminator`: Identifies the data type for programs to correctly deserialize account data. Similar to Anchor's 8-byte account discriminator.
* `data`: Stores the account's current state as arbitrary bytes (e.g., serialized user data, balances, metadata).
* Compressed accounts have no fixed maximum size to store data like Solana's 10 MB.
* Still, Solana's 1,232-byte transaction limit constrains practical data size to roughly 1 KB per account.
* `data_hash`: Hash of the `data` field (32 bytes).
* When computing the compressed account hash for the state tree, the protocol uses this fixed-size hash instead of the variable-length data bytes.
# Next Steps
# Considerations
Source: https://www.zkcompression.com/learn/considerations
Overview to considerations of ZK Compression, namely larger transaction size, higher compute unit usage, and per-transaction state cost.
# Overview
The sections below describe **compressed account** tradeoffs: larger tx size, higher CU when reading/writing state trees, and per-transaction state cost. These apply when your transaction reads or writes compressed state (e.g. Compressed Tokens or Compressed PDAs).
| Primitive | Tx size / CU / State cost | More |
| :------------------------------------ | :----------------------------------------- | :--------------------------------------------------------------------------------------------- |
| **Compressed Token / Compressed PDA** | The limits below apply to every operation. | [Compressed Tokens](/compressed-tokens/overview), [Compressed PDAs](/compressed-pdas/overview) |
# Limitations of compressed account operations
When your transaction reads or writes compressed state, consider:
* Larger Transaction Size
* High Compute Unit Usage
* Per-Transaction State Cost
# General Recommendation
Consider which accounts in your application benefit from ZK Compression and which don't.
* You can use both types for different parts of your application.
* You can decompress and compress accounts at will.
It may be preferred for an account *not* to be permanently compressed if:
* The account gets updated very frequently within a single block (e.g., shared liquidity pools in a DeFi protocol).
* You expect the lifetime number of writes to the same account to be very large (>>1000x).
* The account stores large amounts of data, and you need to access a large part of it (>1kb) inside one on-chain transaction.
# Larger Transaction Size
Solana's transaction size limit is 1232 Bytes. When your transaction **reads or writes compressed state**, size increases in two ways:
* 128 bytes must be reserved for the validity proof, which is a constant size per transaction, assuming the transaction reads from at least one compressed account.
* For V2: The real byte overhead is 1 or 129 bytes, depending on how cold the account is. For V1: it's always 128 bytes.
* You must send the account data you want to read/write on-chain.
# High Compute Unit Usage
System CU usage when reading/writing **compressed state**:
* \~100,000 CU for validity proof verification (constant per transaction that touches compressed state)
* \~100,000 CU system use (state tree [Poseidon](https://eprint.iacr.org/2019/458.pdf) hashing et al.)
* \~6,000 CU per compressed account read/write
**Example**: a typical compressed token transfer uses around 292,000 CU.
Higher CU usage can:
* **Lead to usage limits:** The total CU limit per transaction is 1,400,000 CU, and the per-block write lock limit per State tree is 12,000,000 CU.
* **Require your users to increase their** [**priority fee**](https://solana.com/developers/guides/advanced/how-to-use-priority-fees) **during congestion:** Whenever Solana's global per-block CU limit (48,000,000 CU) is reached, validator clients may prioritize transactions with higher per-CU priority fees.
# State Cost per Transaction
Writing compressed account state costs a **network fee per state tree** plus **rollover per new leaf**. New addresses cost a one-time fee. All amounts are in addition to Solana's base fee.
| What | Lamports | When |
| :------------------ | :------------------ | :--------------------------------------------------------------------------------------------------------------------------------------- |
| Solana base fee | 5,000 per signature | Every transaction |
| State tree (V2) | 5,000 per tree | Once per tree per instruction, whether you read, write, or both. |
| State tree (V1) | 5,000 per tree | Charged separately for trees you **read from** (nullify) and trees you **write to** (create). So 1 read + 1 write on same tree = 10,000. |
| New leaf (rollover) | \~300 per leaf | Each new state leaf (any tree; default depth 26). |
| New address | 10,000 per address | One-time when creating an address (v2 address tree). |
**In practice:** A transfer that updates one leaf on one tree is **5,000 + 300** lamports (V2) or **10,000 + 300** (V1). Creating 10 new outputs on one tree is **5,000 + 3,000** (same for both).
# Next Steps
You're ready to take the next step and start building!
# Merkle trees and Validity Proofs
Source: https://www.zkcompression.com/learn/merkle-trees-validity-proofs
Learn the core concepts of state trees, address trees, and validity proofs for compressed accounts.
# Overview
The protocol uses two types of Merkle trees that serve different purposes:
* [**State trees**](#state-trees) store compressed account hashes
* [**Address trees**](#address-merkle-trees) store addresses that serve as persistent identifiers
The protocol maintains multiple Merkle trees to reduce write-lock contention. Solana's runtime locks accounts during writes, wherefore a single tree would become a bottleneck. Multiple trees allow parallel transactions.
Developers don't need to maintain or initialize Merkle trees themselves. The protocol maintains [Merkle trees at these addresses](/resources/addresses-and-urls#state-trees-&-queues-&-cpi-accounts).
# State trees
A state tree is a binary Merkle tree that stores data of millions of compressed Solana accounts in leaves for efficient cryptographic verification the integrity of all leaves in a tree.
### Merkle Tree Structure
A Merkle tree compresses data by hashing adjacent leaves repeatedly into a single root hash, starting from the lowest level. The hash of a compressed Solana account is stored as a leaf in a State tree.
### Merkle Root Hash
Only this root hash is stored on chain as single value on chain to secure the integrity of all compressed state in a tree. The raw state can thus be stored as calldata in the much cheaper Solana ledger space while preserving Solana's security guarantees.
### Leaf Hash Structure: Compressed Account Hashes
For compressed Solana accounts, the 32 byte leaf hashes effectively mirror the regular Solana account layout: `{DataHash, StateHash; Owner, Lamports`.
The `data_hash` represents the fingerprint of the actual account data.
The `state_hash` ensures that each account hash is globally unique. It includes
* the public key of the state tree's respective on-chain account (i.e., `state_tree_hash`) and
* the compressed account's position in the tree (i.e., `leafIndex`).
Lastly, `owner_hashed` determines which program owns this account and `lamports` show the account balance.
For [details on the compressed account structure see this section](/learn/compressed-account-model#compressed-account-structure).
### Merkle and Validity Proofs
In state trees, a validity proof verifies that a compressed account exists in state trees with a constant 128-byte proof size. This proof must be included in every transaction once a compressed account is created to verify the on-chain state.
Developers don't need to generate validity proofs or learn about ZK to use ZK Compression.
ZK Compression uses [Groth16](https://docs.rs/groth16-solana/latest/groth16_solana/), a well-known [pairing-based](https://en.wikipedia.org/wiki/Pairing-based_cryptography) [zk-SNARK](https://www.helius.dev/blog/zero-knowledge-proofs-its-applications-on-solana#-zk-snarks-and-circuits), for its proof system.
The validity proof contains one or more merkle proofs:
* Merkle proofs consist of sibling node hashes along the path from the account's leaf to the root.
* Starting with the leaf hash, the verifier calculates up the tree using these sibling hashes. The proof path is shown with three highlighted elements in a box at the bottom right: Leaf 0, Node 1, and Node 5, representing the sibling hashes needed to verify Leaf 1.
* If the calculated root matches the on-chain root, the account is verified.
For a tree with height 26, a single proof requires 26 sibling hashes (32 bytes each) plus metadata totaling 832 bytes. The proof size grows with tree height.
ZK Compression batches multiple Merkle proofs into a single zero-knowledge proof to achieve a constant 128-byte size regardless of how many accounts are verified:
| Accounts Verified | Proof Components | Size |
| :---------------- | :-------------------- | :--------------------------- |
| 1 | 1 merkle proof | 832 bytes |
| 1 | 1 merkle + 1 ZK proof | 128 bytes |
| 8 | 8 merkle + 1 ZK proof | 128 bytes |
Two **state tree versions** with different proof mechanisms are currently supported:
* **V1 state trees**: Always require the full 128-byte ZK proof
* **V2 batched state trees**: Can use `prove_by_index` optimization that verifies the account exists with only one byte instead of 128-bytes.
**V2** optimizes compute unit consumption by up to 70%.
When using V2 trees, RPC requests automatically choose the proof mechanism.
# Address trees
Address trees store addresses that serve as optional, persistent identifiers for compressed accounts.
Every address is unique within its address tree, but the same seeds can create different addresses in different address trees. To enforce that a compressed account can only be created once with the same seed, check the address tree pubkey in your program.
### Address Tree Structure
Address trees store derived addresses in an indexed structure. Unlike state trees that store account hashes, address trees store the actual address values along with pointers to maintain sorted order.
These addresses are used only when compressed accounts require a persistent identifier that doesn't change when the account data updates.
### Address Tree Root Hash
Like state trees, only the root hash is stored on-chain to verify all addresses in the tree. The raw addresses are stored in the Solana ledger.
### Merkle and Validity Proofs
In address trees, validity proofs verify when creating a compressed account with an address that the address doesn't already exist in a specified address tree. This constant 128-byte proof must be included in the transaction only when creating accounts with addresses, not for subsequent compressed account interactions.
Two **address tree versions** are currently supported:
* **V1 address trees** height 26 (\~67 million addresses).
* **V2 batched address trees** with height 40 (\~1 trillion addresses).
**V2** optimizes compute unit consumption by up to 70%.
When using V2 trees, RPC requests automatically choose the proof mechanism.
# Resources on ZK
For those interested in learning more about the fundamentals of ZK and its applications on Solana, we recommend reading the following:
# Next Steps
# High-level System Overview
Source: https://www.zkcompression.com/learn/overview
Overview to ZK Compression's Core Concepts. Get a high-level system overview and learn about the compressed account model, lifecycle of a transaction, and considerations.
## What is ZK Compression?
ZK Compression is a Solana account primitive that lets you create tokens and PDAs without rent-exemption cost with L1 performance and security.
Compressed tokens use ZK Compression under the hood.
Storage cost of Solana accounts are reduced by combining generalized state compression and zero-knowledge proofs.
State compression stores account data on the Solana ledger instead of in millions of on-chain accounts. Only a small fingerprint of all accounts is stored on-chain for cryptographic integrity. Using Light Protocol, programs can use the account data inside Solana's virtual machine as if it were stored on-chain.
The protocol uses 128 byte zero-knowledge proofs (validity proofs) to verify the integrity of
the compressed accounts. By default, this is all done under the hood. You can fetch validity
proofs from RPC providers that support ZK Compression.
### Storage of Compressed State
Compressed accounts store state as call data on Solana's ledger.
This maintains Solana's security and removes the need for rent-exemption per account.
### Accessing Compressed State
Transactions specify state they access (read/write) and include it in the transaction payload.
### State Validation
Solana Programs invoke the Light System Program to update compressed state
1. **The protocol validates the state** (validity of existing state, sum checks, ownership checks)
2. **The protocol enforces a schema:** Classic Accounts → Compressed Accounts.\
Compressed accounts have a layout similar to classic accounts.
### State Updates
The new state is recorded as a log on the Solana ledger with each transaction.
### RPC Nodes
Photon RPC nodes
index the state changes and make the compressed account state available to clients via the ZK Compression RPC API.
### Forester Nodes
Keeper nodes that interact with the Account Compression Program to empty queues and rollover state trees.
Clients and custom programs do not interact with Forester nodes or the Account Compression Program. This is all done under the hood.
# Next Steps
In the following sections, we'll dive into the core concepts that make ZK Compression possible:
* **Compressed Account Model**: Understand how compressed accounts differ from regular Solana accounts (not much!) and how they enhance efficiency.
* **State Trees**: Learn about the Merkle tree structure used to store compressed accounts and how it minimizes on-chain storage.
* **Validity Proofs**: Learn how zero-knowledge proofs verify data efficiently while keeping proof sizes small.
* **Lifecycle of a Transaction**: Follow the journey of a transaction from creation to execution with ZK Compression.
* **Considerations**: Discover the trade-offs and scenarios where ZK Compression may or may not be the best solution.
# Lifecycle of a Transaction
Source: https://www.zkcompression.com/learn/transaction-lifecycle
Overview to the lifecycle of a transaction that interacts with compressed accounts.
# Overview
This guide assumes you are familiar with transactions on Solana. If you aren't, we recommend to read the [Solana documentation on transactions](https://solana.com/docs/core/transactions).
Transactions to interact with compressed accounts are fully compatible with Solana's Transaction and Versioned Transaction formats.
There are few nuances to build transactions with compressed accounts as compared to regular accounts:
* Instructions must specify the list of all compressed accounts being read or written to.
* To read or write to a compressed account, the instruction must send the current account state on-chain and prove its validity.
* Each unique state tree that gets read or written to (via any compressed account) needs to be specified as per Solana's regular on-chain [account access lists](https://solana.com/docs/core/transactions#array-of-account-addresses).
# Reading Compressed Accounts
Reading compressed accounts follows a similar pattern to Solana accounts.
* The main difference is that compressed account RPC methods query an indexer instead of the ledger directly.
* The indexer, called Photon, reconstructs compressed account state from the Solana ledger by reading transaction logs.
The API exposed by the indexer closely mirrors existing RPC calls, with one-to-one mapping:
| Solana RPC | Photon RPC Calls |
| :---------------------- | :-------------------------------- |
| getAccountInfo | getCompressedAccount |
| getBalance | getCompressedBalance |
| getTokenAccountsByOwner | getCompressedTokenAccountsByOwner |
| getProgramAccounts | getCompressedAccountsByOwner |
Clients read compressed accounts similar to Solana accounts:
1. Fetch the compressed account data from your RPC provider using its address or hash
* Here you use [`getCompressedAccount()`](/api-reference/json-rpc-methods/getcompressedaccount), similar to `getAccountInfo()`.
2. Deserialize the account's data field into the appropriate data structure.
```typescript TypeScript theme={null}
// 1. Fetch compressed account from indexer
const compressedAccount = await rpc.getCompressedAccount(
bn(address.toBytes())
);
// 2. Deserialize account data
const accountData = coder.types.decode(
"AccountType",
compressedAccount.data.data
);
```
```rust Rust theme={null}
// 1. Fetch compressed account from indexer
let compressed_account = rpc
.get_compressed_account(address, None)
.await?
.value
.unwrap();
// 2. Deserialize account data
let account_data = deserialize(&compressed_account.data)?;
```
On-chain reading within programs requires a validity proof to verify the account exists in the state tree.
1. Fetch the compressed account data from your RPC provider using its address or hash
2. Fetch a validity proof using the account hash via [`getValidityProof()`](/api-reference/json-rpc-methods/getvalidityproof).
3. Clients pass the proof to the on-chain program in the instruction data.
```typescript TypeScript theme={null}
// 1. Fetch compressed account from indexer
const compressedAccount = await rpc.getCompressedAccount(
bn(address.toBytes())
);
// 2. Get validity proof using account hash
const proof = await rpc.getValidityProof(
[bn(compressedAccount.hash)]
);
// 3. Proof is included in transaction instructions
```
```rust Rust theme={null}
// 1. Fetch compressed account from indexer
let compressed_account = rpc
.get_compressed_account(address, None)
.await?
.value
.unwrap();
// 2. Get validity proof using account hash
let proof = rpc
.get_validity_proof(vec![compressed_account.hash], vec![], None)
.await?
.value;
// 3. Proof is included in transaction instructions
```
See this [read-only program example](https://github.com/Lightprotocol/program-examples/tree/main/read-only) implementation.
# Writing to Compressed Accounts
Writing to compressed accounts can be described more generally as:
```bash theme={null}
(state, validityProof) -> state transition -> state'
```
**Writing to a compressed account involves these steps:**
1. Fetch the compressed account data from your RPC provider using its address or hash
2. Fetch a validity proof from your RPC provider using the account hash via [`getValidityProof()`](/api-reference/json-rpc-methods/getvalidityproof) to prove the account hash exists in the state tree.
The Solana program executing the state transition ***Data***\*\* -> **\_**Data'\*\*\_ requires its client to pack the instructions with:
* `address`: persistent identifier of the compressed account (unchanged)
* `owner program`: program ID that owns this account (unchanged)
* `data`: current account data
* `data'`: updated account data
* `validity proof`: 128-byte ZK proof that verifies the current account hash exists in the state tree
See this [guide to update a compressed account](/compressed-pdas/guides/how-to-update-compressed-accounts).
# On-chain Execution
To write compressed state, a program invokes the Light System Program via CPI. The system program then does the following:
1. Runs relevant checks ([sum check](https://github.com/Lightprotocol/light-protocol/blob/v.1.0.0/programs/system/src/invoke/verify_state_proof.rs#L204-L210), etc.)
2. [Verifies the validity proof](https://github.com/Lightprotocol/light-protocol/blob/v.1.0.0/programs/system/src/invoke/processor.rs#L209-L214)
3. [Nullifies](https://github.com/Lightprotocol/light-protocol/blob/v.1.0.0/programs/system/src/invoke/processor.rs#L209-L214) the existing leaf of the compressed account that is being written to, to prevent double spending.
4. [Appends](https://github.com/Lightprotocol/light-protocol/blob/v.1.0.0/programs/system/src/invoke/processor.rs#L245-L254) the new compressed account hash to the state tree and advances the tree's state root
5. [Emits](https://github.com/Lightprotocol/light-protocol/blob/v.1.0.0/programs/system/src/invoke/processor.rs#L272-L279) the new compressed account state onto the Solana ledger
An RPC node then parses the transaction and compressed state and provides the read state to clients via the ZK Compression RPC API.
# Next Steps
# Addresses and URLs
Source: https://www.zkcompression.com/resources/addresses-and-urls
Overview to all of ZK Compression's RPC URLs, Program IDs & Accounts and Lookup Tables.
## RPC URLs
| Network | Service | URL |
| :------ | :-------------------- | :------------------------------------------------------ |
| Mainnet | Network Address (RPC) | `https://mainnet.helius-rpc.com?api-key=<api_key>` |
| Mainnet | Photon RPC API | `https://mainnet.helius-rpc.com?api-key=<api_key>` |
| Network | Service | URL |
| :------ | :-------------------- | :----------------------------------------------------- |
| Devnet | Network Address (RPC) | `https://devnet.helius-rpc.com?api-key=<api_key>` |
| Devnet | Photon RPC API | `https://devnet.helius-rpc.com?api-key=<api_key>` |
Find all JSON RPC Methods for ZK Compression [here](/api-reference/json-rpc-methods/overview).
## Program IDs
| | |
| :-------------------------- | :---------------------------------------------- |
| Light System Program | **SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7** |
| Light Token Program | **cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m** |
| Account Compression Program | **compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq** |
## State Trees & Queues & CPI Accounts
**In your local test validator environment** use in Rust `TestAccounts::get_local_test_validator_accounts()` to get all pre-configured protocol, state tree, and address tree accounts.
| | Public Key |
| :-------------- | :---------------------------------------------- |
| #1 State Tree | **bmt1LryLZUMmF7ZtqESaw7wifBXLfXHQYoE4GAmrahU** |
| #1 Output Queue | **oq1na8gojfdUhsfCpyjNt6h4JaDWtHf1yQj4koBWfto** |
| #1 CPI Context | **cpi15BoVPKgEPw5o8wc2T816GE7b378nMXnhH3Xbq4y** |
| | Public Key |
| :-------------- | :---------------------------------------------- |
| #2 State Tree | **bmt2UxoBxB9xWev4BkLvkGdapsz6sZGkzViPNph7VFi** |
| #2 Output Queue | **oq2UkeMsJLfXt2QHzim242SUi3nvjJs8Pn7Eac9H9vg** |
| #2 CPI Context | **cpi2yGapXUR3As5SjnHBAVvmApNiLsbeZpF3euWnW6B** |
| | Public Key |
| :-------------- | :---------------------------------------------- |
| #3 State Tree | **bmt3ccLd4bqSVZVeCJnH1F6C8jNygAhaDfxDwePyyGb** |
| #3 Output Queue | **oq3AxjekBWgo64gpauB6QtuZNesuv19xrhaC1ZM1THQ** |
| #3 CPI Context | **cpi3mbwMpSX8FAGMZVP85AwxqCaQMfEk9Em1v8QK9Rf** |
| | Public Key |
| :-------------- | :---------------------------------------------- |
| #4 State Tree | **bmt4d3p1a4YQgk9PeZv5s4DBUmbF5NxqYpk9HGjQsd8** |
| #4 Output Queue | **oq4ypwvVGzCUMoiKKHWh4S1SgZJ9vCvKpcz6RT6A8dq** |
| #4 CPI Context | **cpi4yyPDc4bCgHAnsenunGA8Y77j3XEDyjgfyCKgcoc** |
| | Public Key |
| :-------------- | :---------------------------------------------- |
| #5 State Tree | **bmt5yU97jC88YXTuSukYHa8Z5Bi2ZDUtmzfkDTA2mG2** |
| #5 Output Queue | **oq5oh5ZR3yGomuQgFduNDzjtGvVWfDRGLuDVjv9a96P** |
| #5 CPI Context | **cpi5ZTjdgYpZ1Xr7B1cMLLUE81oTtJbNNAyKary2nV6** |
| | Public Key |
| :-------------- | :---------------------------------------------- |
| State Tree | **smt2rJAFdyJJupwMKAqTNAJwvjhmiZ4JYGZmbVRw1Ho** |
| Nullifier Queue | **nfq2hgS7NYemXsFaFUCe3EMXSDSfnZnAe27jC6aPP1X** |
| CPI Context | **cpi2cdhkH5roePvcudTgUL8ppEBfTay1desGh8G8QxK** |
## Address Trees & Queues
| Address Tree | |
| :----------- | :---------------------------------------------- |
| Address Tree | **amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx** |
| Address Tree #1 | |
| :--------------- | :---------------------------------------------- |
| Address Tree #1 | **amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2** |
| Address Queue #1 | **aq1S9z4reTSQAdgWHGD2zDaS39sjGrAxbR31vxJ2F4F** |
## Interface PDA
The account to convert format between:
* compressed token ↔ SPL token
* compressed token ↔ SPL token
| | |
| :------------ | :----------------------------------------------- |
| Interface PDA | **GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy** |
## Lookup Tables
[Lookup tables](https://solana.com/docs/advanced/lookup-tables) reduce your transaction size. We provide pre-initialized lookup tables that cover the Light's program IDs and accounts:
| | |
| :------------------------ | :----------------------------------------------- |
| Lookup Table #1 (Mainnet) | **9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ** |
| Lookup Table #1 (Devnet) | **qAJZMgnQJ8G6vA3WRcjD9Jan1wtKkaCFWLWskxJrR5V** |
We provide a helper function below if you need to extend a custom lookup table.
```typescript theme={null}
import { Rpc, confirmTx, createRpc } from "@lightprotocol/stateless.js";
import { createTokenProgramLookupTable } from "@lightprotocol/compressed-token";
import { Keypair, PublicKey} from "@solana/web3.js";
import { RPC_ENDPOINT } from "./constants";
const payer = Keypair.generate();
const authority = payer;
const additionalTokenMints : PublicKey[] = [];
const additionalAccounts : PublicKey[] = [];
// Localnet
const connection: Rpc = createRpc();
const main = async () => {
/// airdrop lamports to pay gas and rent
await confirmTx(
connection,
await connection.requestAirdrop(payer.publicKey, 1e7)
);
/// Create LUT
const { address } = await createTokenProgramLookupTable(
connection,
payer,
authority,
additionalTokenMints,
additionalAccounts
);
console.log("Created lookup table:", address.toBase58());
};
main();
```
## System Accounts List
|
|
|
| 1 |
Light System Program |
Verifies validity proofs, compressed account ownership checks, and CPIs the Account Compression Program to update tree accounts. |
| 2 |
CPI 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.
|
| 3 |
Registered Program PDA |
Provides access control to the Account Compression Program. |
| 4 |
Account Compression Authority |
Signs CPI calls from the Light System Program to the Account Compression Program. |
| 5 |
Account Compression Program |
* Writes to state and address tree accounts.
* Clients and the Account Compression Program do not interact directly — handled internally.
|
| 6 |
System Program |
Solana System Program used to transfer lamports. |
***
## Didn't find what you were looking for?
Reach out! [Telegram](https://t.me/swen_light) | [email](mailto:support@lightprotocol.com) | [Discord](https://discord.com/invite/7cJ8BhAXhu)
# Error Cheatsheet
Source: https://www.zkcompression.com/resources/error-cheatsheet
Complete error code reference for ZK Compression. Search error codes with hex values and messages.
## How to use this page
1. **Search your error code or hex value** with `Cmd+F`/ `Ctrl+F`
2. **Check for debug guides -** some errors have detailed troubleshooting pages
3. **Still having issues?** We're here to help!
* Reach out on [Discord](https://discord.com/invite/CYvjBgzRFP) for support
* Share the exact error code and a reproducer (GitHub repo or [Gist](https://gist.github.com/))
For help with debugging use the [MCP Configuration](/ai-tools/mcp) or AskDevin via
## 6000 - 6053 / SystemProgramError Variants
Source code:
[https://github.com/Lightprotocol/light-protocol/blob/main/programs/system/src/errors.rs](https://github.com/Lightprotocol/light-protocol/blob/604892ff3902292dd0b0b047cb0bfacc469ea0d4/programs/system/src/errors.rs#L133)
| Code | Hex | Error | Message |
| :--- | :----- | :---------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------- |
| 6000 | 0x1770 | `SumCheckFailed` | "Sum check failed" |
| 6001 | 0x1771 | `SignerCheckFailed` | "Signer check failed" |
| 6002 | 0x1772 | `CpiSignerCheckFailed` | "Cpi signer check failed" |
| 6003 | 0x1773 | `ComputeInputSumFailed` | "Computing input sum failed." |
| 6004 | 0x1774 | `ComputeOutputSumFailed` | "Computing output sum failed." |
| 6005 | 0x1775 | `ComputeRpcSumFailed` | "Computing rpc sum failed." |
| 6006 | 0x1776 | `InvalidAddress` | "InvalidAddress" |
| 6007 | 0x1777 | `DeriveAddressError` | "DeriveAddressError" |
| 6008 | 0x1778 | `CompressedSolPdaUndefinedForCompressSol` | "CompressedSolPdaUndefinedForCompressSol" |
| 6009 | 0x1779 | `DecompressLamportsUndefinedForCompressSol` | "DecompressLamportsUndefinedForCompressSol" |
| 6010 | 0x177A | `CompressedSolPdaUndefinedForDecompressSol` | "CompressedSolPdaUndefinedForDecompressSol" |
| 6011 | 0x177B | `DeCompressLamportsUndefinedForDecompressSol` | "DeCompressLamportsUndefinedForDecompressSol" |
| 6012 | 0x177C | `DecompressRecipientUndefinedForDecompressSol` | "DecompressRecipientUndefinedForDecompressSol" |
| 6013 | 0x177D | `WriteAccessCheckFailed` | "WriteAccessCheckFailed" |
| 6014 | 0x177E | `InvokingProgramNotProvided` | "InvokingProgramNotProvided" |
| 6015 | 0x177F | `InvalidCapacity` | "InvalidCapacity" |
| 6016 | 0x1780 | `InvalidMerkleTreeOwner` | "InvalidMerkleTreeOwner" |
| 6017 | 0x1781 | `ProofIsNone` | "ProofIsNone" |
| 6018 | 0x1782 | `ProofIsSome` | "Proof is some but no input compressed accounts or new addresses provided." |
| 6019 | 0x1783 | `EmptyInputs` | "EmptyInputs" |
| 6020 | 0x1784 | `CpiContextAccountUndefined` | "CpiContextAccountUndefined" |
| 6021 | 0x1785 | `CpiContextEmpty` | "CpiContextEmpty" |
| 6022 | 0x1786 | `CpiContextMissing` | "CpiContextMissing" |
| 6023 | 0x1787 | `DecompressionRecipientDefined` | "DecompressionRecipientDefined" |
| 6024 | 0x1788 | `SolPoolPdaDefined` | "SolPoolPdaDefined" |
| 6025 | 0x1789 | `AppendStateFailed` | "AppendStateFailed" |
| 6026 | 0x178A | `InstructionNotCallable` | "The instruction is not callable" |
| 6027 | 0x178B | `CpiContextFeePayerMismatch` | "CpiContextFeePayerMismatch" |
| 6028 | 0x178C | `CpiContextAssociatedMerkleTreeMismatch` | "CpiContextAssociatedMerkleTreeMismatch" |
| 6029 | 0x178D | `NoInputs` | "NoInputs" |
| 6030 | 0x178E | `InputMerkleTreeIndicesNotInOrder` | "Input merkle tree indices are not in ascending order." |
| 6031 | 0x178F | `OutputMerkleTreeIndicesNotInOrder` | "Output merkle tree indices are not in ascending order." |
| 6032 | 0x1790 | `OutputMerkleTreeNotUnique` | "OutputMerkleTreeNotUnique" |
| 6033 | 0x1791 | `DataFieldUndefined` | "DataFieldUndefined" |
| 6034 | 0x1792 | `ReadOnlyAddressAlreadyExists` | "ReadOnlyAddressAlreadyExists" |
| 6035 | 0x1793 | `ReadOnlyAccountDoesNotExist` | "ReadOnlyAccountDoesNotExist" |
| 6036 | 0x1794 | `HashChainInputsLenghtInconsistent` | "HashChainInputsLenghtInconsistent" |
| 6037 | 0x1795 | `InvalidAddressTreeHeight` | "InvalidAddressTreeHeight" |
| 6038 | 0x1796 | `InvalidStateTreeHeight` | "InvalidStateTreeHeight" |
| 6039 | 0x1797 | `InvalidArgument` | "InvalidArgument" |
| 6040 | 0x1798 | `InvalidAccount` | "InvalidAccount" |
| 6041 | 0x1799 | `AddressMerkleTreeAccountDiscriminatorMismatch` | "AddressMerkleTreeAccountDiscriminatorMismatch" |
| 6042 | 0x179A | `StateMerkleTreeAccountDiscriminatorMismatch` | "StateMerkleTreeAccountDiscriminatorMismatch" |
| 6043 | 0x179B | `ProofVerificationFailed` | "Proof verification failed."
[How to debug this error](/resources/error-cheatsheet/debug-0x179b-6043-proofverificationfailed) |
| 6044 | 0x179C | `InvalidAccountMode` | "Invalid account mode." |
| 6045 | 0x179D | `InvalidInstructionDataDiscriminator` | "InvalidInstructionDataDiscriminator" |
| 6046 | 0x179E | `NewAddressAssignedIndexOutOfBounds` | "NewAddressAssignedIndexOutOfBounds" |
| 6047 | 0x179F | `AddressIsNone` | "AddressIsNone" |
| 6048 | 0x17A0 | `AddressDoesNotMatch` | "AddressDoesNotMatch" |
| 6049 | 0x17A1 | `CpiContextAlreadySet` | "CpiContextAlreadySet" |
| 6050 | 0x17A2 | `InvalidTreeHeight` | "InvalidTreeHeight" |
| 6051 | 0x17A3 | `TooManyOutputAccounts` | "TooManyOutputAccounts" |
| 6052 | 0x17A4 | `BorrowingDataFailed` | "Borrowing data failed" |
| 6053 | 0x17A5 | `DuplicateAccountInInputsAndReadOnly` | "DuplicateAccountInInputsAndReadOnly" |
## 7001 - 7009 / HasherError Variants
Source code:
[https://github.com/Lightprotocol/light-protocol/blob/program-libs/hasher/src/errors.rs](https://github.com/Lightprotocol/light-protocol/blob/604892ff3902292dd0b0b047cb0bfacc469ea0d4/program-libs/hasher/src/errors.rs)
| Code | Hex | Error | Message |
| :--- | :----- | :-------------------------------------- | :---------------------------------------------------------------------------------------- |
| 7001 | 0x1B59 | `IntegerOverflow` | "Integer overflow, value too large" |
| 7003 | 0x1B5B | `PoseidonSyscall(PoseidonSyscallError)` | "Poseidon syscall error: " |
| 7005 | 0x1B5D | `InvalidInputLength(usize, usize)` | "Allowed input length provided " |
| 7006 | 0x1B5E | `InvalidNumFields` | "Invalid number of fields" |
| 7007 | 0x1B5F | `EmptyInput` | "Empty input" |
| 7008 | 0x1B60 | `BorshError` | "Borsh serialization failed." |
| 7009 | 0x1B61 | `OptionHashToFieldSizeZero` | "Option hash to field size returned \[0u8;32], a collision with None for an Option type." |
## 10001 - 10014 / ConcurrentMerkleTreeError Variants
Source code:
[https://github.com/Lightprotocol/light-protocol/blob/program-libs/concurrent-merkle-tree/src/errors.rs](https://github.com/Lightprotocol/light-protocol/blob/604892ff3902292dd0b0b047cb0bfacc469ea0d4/program-libs/concurrent-merkle-tree/src/errors.rs)
| Code | Hex | Error | Message |
| :---- | :----- | :---------------------------------------- | :---------------------------------------------------------------------------------------------------------------- |
| 10001 | 0x2711 | `IntegerOverflow` | "Integer overflow" |
| 10002 | 0x2712 | `HeightZero` | "Invalid height, it has to be greater than 0" |
| 10003 | 0x2713 | `InvalidHeight(usize)` | "Invalid height, expected " |
| 10004 | 0x2714 | `ChangelogZero` | "Invalid changelog size, it has to be greater than 0. Changelog is used for storing Merkle paths during appends." |
| 10005 | 0x2715 | `RootsZero` | "Invalid number of roots, it has to be greater than 0" |
| 10006 | 0x2716 | `CanopyGeThanHeight` | "Canopy depth has to be lower than height" |
| 10007 | 0x2717 | `TreeIsFull` | "Merkle tree is full, cannot append more leaves." |
| 10008 | 0x2718 | `BatchGreaterThanChangelog(usize, usize)` | "Number of leaves () exceeds the changelog capacity ()." |
| 10009 | 0x2719 | `InvalidProofLength(usize, usize)` | "Invalid proof length, expected , got ." |
| 10010 | 0x271A | `InvalidProof([u8; 32], [u8; 32])` | "Invalid Merkle proof, expected root: `{0:?}`, the provided proof produces root: `{1:?}`" |
| 10011 | 0x271B | `CannotUpdateLeaf` | "Attempting to update the leaf which was updated by an another newest change." |
| 10012 | 0x271C | `CannotUpdateEmpty` | "Cannot update the empty leaf" |
| 10013 | 0x271D | `EmptyLeaves` | "The batch of leaves is empty" |
| 10014 | 0x271E | `BufferSize(usize, usize)` | "Invalid buffer size, expected , got " |
## 11001 - 11009 / IndexedMerkleTreeError Variants
Source code:
[https://github.com/Lightprotocol/light-protocol/blob/program-libs/indexed-merkle-tree/src/errors.rs](https://github.com/Lightprotocol/light-protocol/blob/604892ff3902292dd0b0b047cb0bfacc469ea0d4/program-libs/indexed-merkle-tree/src/errors.rs)
| Code | Hex | Error | Message |
| :---- | :----- | :-------------------------------------- | :------------------------------------------------------------------ |
| 11001 | 0x2AF9 | `IntegerOverflow` | "Integer overflow" |
| 11002 | 0x2AFA | `IndexHigherThanMax` | "Invalid index, it exceeds the number of elements." |
| 11003 | 0x2AFB | `LowElementNotFound` | "Could not find the low element." |
| 11004 | 0x2AFC | `LowElementGreaterOrEqualToNewElement` | "Low element is greater or equal to the provided new element." |
| 11005 | 0x2AFD | `NewElementGreaterOrEqualToNextElement` | "The provided new element is greater or equal to the next element." |
| 11006 | 0x2AFE | `ElementAlreadyExists` | "The element already exists, but was expected to be absent." |
| 11007 | 0x2AFF | `ElementDoesNotExist` | "The element does not exist, but was expected to be present." |
| 11008 | 0x2B00 | `ChangelogBufferSize(usize, usize)` | "Invalid changelog buffer size, expected , got " |
| 11009 | 0x2B01 | `ArrayFull` | "Indexed array is full, cannot append more elements" |
## 12006 - 12019 / AccountError Variants
Source code:
[https://github.com/Lightprotocol/light-protocol/blob/program-libs/account-checks/src/error.rs](https://github.com/Lightprotocol/light-protocol/blob/604892ff3902292dd0b0b047cb0bfacc469ea0d4/program-libs/account-checks/src/error.rs)
| Code | Hex | Error | Message |
| :---- | :----- | :--------------------------- | :-------------------------------- |
| 12006 | 0x2EE6 | `InvalidDiscriminator` | "Invalid Discriminator." |
| 12007 | 0x2EE7 | `AccountOwnedByWrongProgram` | "Account owned by wrong program." |
| 12008 | 0x2EE8 | `AccountNotMutable` | "Account not mutable." |
| 12009 | 0x2EE9 | `BorrowAccountDataFailed` | "Borrow account data failed." |
| 12010 | 0x2EEA | `InvalidAccountSize` | "Invalid Account size." |
| 12011 | 0x2EEB | `AccountMutable` | "Account is mutable." |
| 12012 | 0x2EEC | `AlreadyInitialized` | "Account is already initialized." |
| 12013 | 0x2EED | `InvalidAccountBalance` | "Invalid account balance." |
| 12014 | 0x2EEE | `FailedBorrowRentSysvar` | "Failed to borrow rent sysvar." |
| 12015 | 0x2EEF | `InvalidSigner` | "Invalid Signer" |
| 12016 | 0x2EF0 | `InvalidSeeds` | "Invalid Seeds" |
| 12017 | 0x2EF1 | `InvalidProgramId` | "Invalid Program Id" |
| 12018 | 0x2EF2 | `ProgramNotExecutable` | "Program not executable." |
| 12019 | 0x2EF3 | `AccountNotZeroed` | "Account not zeroed." |
## 14001 - 14009 / MerkleTreeMetadataError Variants
Source code:
[https://github.com/Lightprotocol/light-protocol/blob/program-libs/merkle-tree-metadata/src/errors.rs](https://github.com/Lightprotocol/light-protocol/blob/604892ff3902292dd0b0b047cb0bfacc469ea0d4/program-libs/merkle-tree-metadata/src/errors.rs)
| Code | Hex | Error | Message |
| :---- | :----- | :-------------------------------- | :------------------------------------------ |
| 14001 | 0x36B1 | `MerkleTreeAndQueueNotAssociated` | "Merkle tree and queue are not associated." |
| 14002 | 0x36B2 | `RolloverNotConfigured` | "Rollover not configured." |
| 14003 | 0x36B3 | `MerkleTreeAlreadyRolledOver` | "Merkle tree already rolled over." |
| 14004 | 0x36B4 | `InvalidQueueType` | "Invalid queue type." |
| 14005 | 0x36B5 | `InsufficientRolloverFee` | "Insufficient rollover fee." |
| 14006 | 0x36B6 | `NotReadyForRollover` | "Merkle tree not ready for rollover." |
| 14007 | 0x36B7 | `InvalidTreeType` | "Invalid tree type." |
| 14008 | 0x36B8 | `InvalidRolloverThreshold` | "Invalid Rollover Threshold." |
| 14009 | 0x36B9 | `InvalidHeight` | "Invalid Height." |
## 14017 - 14034 / LightSdkTypesError Variants
Source code:
[https://github.com/Lightprotocol/light-protocol/blob/sdk-libs/sdk-types/src/error.rs](https://github.com/Lightprotocol/light-protocol/blob/604892ff3902292dd0b0b047cb0bfacc469ea0d4/sdk-libs/sdk-types/src/error.rs#L4)
| Code | Hex | Error | Message |
| :---- | :----- | :----------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ |
| 14017 | 0x36C1 | `FewerAccountsThanSystemAccounts` | "Fewer accounts than system accounts" |
| 14021 | 0x36C5 | `InitAddressIsNone` | "Address is none during initialization" |
| 14022 | 0x36C6 | `InitWithAddressIsNone` | "Address is none during initialization with address" |
| 14023 | 0x36C7 | `InitWithAddressOutputIsNone` | "Output is none during initialization with address" |
| 14024 | 0x36C8 | `MetaMutAddressIsNone` | "Address is none during meta mutation" |
| 14025 | 0x36C9 | `MetaMutInputIsNone` | "Input is none during meta mutation" |
| 14026 | 0x36CA | `MetaMutOutputLamportsIsNone` | "Output lamports is none during meta mutation" |
| 14027 | 0x36CB | `MetaMutOutputIsNone` | "Output is none during meta mutation" |
| 14028 | 0x36CC | `MetaCloseAddressIsNone` | "Address is none during meta close" |
| 14029 | 0x36CD | `MetaCloseInputIsNone` | "Input is none during meta close" |
| 14031 | 0x36CF | `CpiAccountsIndexOutOfBounds(usize)` | "CPI accounts index out of bounds: " |
| 14032 | 0x36D0 | `InvalidCpiContextAccount` | "Invalid CPI context account" |
| 14033 | 0x36D1 | `InvalidSolPoolPdaAccount` | "Invalid sol pool pda account" |
| 14034 | 0x36D2 | `InvalidCpiAccountsOffset` | "CpiAccounts accounts slice starts with an invalid account. It should start with LightSystemProgram SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7." |
## 14301 - 14312 / BatchedMerkleTreeError Variants
Source code:
[https://github.com/Lightprotocol/light-protocol/blob/program-libs/batched-merkle-tree/src/errors.rs](https://github.com/Lightprotocol/light-protocol/blob/604892ff3902292dd0b0b047cb0bfacc469ea0d4/program-libs/batched-merkle-tree/src/errors.rs)
| Code | Hex | Error | Message |
| :---- | :----- | :------------------------------------ | :------------------------------------------------------ |
| 14301 | 0x37DD | `BatchNotReady` | "Batch is not ready to be inserted" |
| 14302 | 0x37DE | `BatchAlreadyInserted` | "Batch is already inserted" |
| 14303 | 0x37DF | `BatchInsertFailed` | "Batch insert failed" |
| 14304 | 0x37E0 | `LeafIndexNotInBatch` | "Leaf index not in batch." |
| 14305 | 0x37E1 | `InvalidNetworkFee` | "Invalid network fee." |
| 14306 | 0x37E2 | `BatchSizeNotDivisibleByZkpBatchSize` | "Batch size not divisible by ZKP batch size." |
| 14307 | 0x37E3 | `InclusionProofByIndexFailed` | "Inclusion proof by index failed." |
| 14308 | 0x37E4 | `InvalidBatchIndex` | "Invalid batch index" |
| 14309 | 0x37E5 | `InvalidIndex` | "Invalid index" |
| 14310 | 0x37E6 | `TreeIsFull` | "Batched Merkle tree is full." |
| 14311 | 0x37E7 | `NonInclusionCheckFailed` | "Value already exists in bloom filter." |
| 14312 | 0x37E8 | `BloomFilterNotZeroed` | "Bloom filter must be zeroed prior to reusing a batch." |
## 15001 - 15017 / ZeroCopyError Variants
Source code:
[https://github.com/Lightprotocol/light-protocol/blob/program-libs/zero-copy/src/errors.rs](https://github.com/Lightprotocol/light-protocol/blob/604892ff3902292dd0b0b047cb0bfacc469ea0d4/program-libs/zero-copy/src/errors.rs)
| Code | Hex | Error | Message |
| :---- | :----- | :------------------------------------------ | :------------------------------------------------------- |
| 15001 | 0x3A99 | `Full` | "The vector is full, cannot push any new elements" |
| 15002 | 0x3A9A | `ArraySize(usize, usize)` | "Requested array of size , but the vector has elements" |
| 15003 | 0x3A9B | `IterFromOutOfBounds` | "The requested start index is out of bounds" |
| 15004 | 0x3A9C | `InsufficientMemoryAllocated(usize, usize)` | "Memory allocated , Memory required " |
| 15006 | 0x3A9E | `UnalignedPointer` | "Unaligned pointer" |
| 15007 | 0x3A9F | `MemoryNotZeroed` | "Memory not zeroed" |
| 15008 | 0x3AA0 | `InvalidConversion` | "Invalid conversion" |
| 15009 | 0x3AA1 | `InvalidData(Infallible)` | "Invalid data" |
| 15010 | 0x3AA2 | `Size` | "Invalid size" |
| 15011 | 0x3AA3 | `InvalidOptionByte(u8)` | "Invalid option byte must be 0 (None) or 1 (Some)" |
| 15012 | 0x3AA4 | `InvalidCapacity` | "Invalid capacity. Capacity must be greater than 0" |
| 15013 | 0x3AA5 | `LengthGreaterThanCapacity` | "Length is greater than capacity" |
| 15014 | 0x3AA6 | `CurrentIndexGreaterThanLength` | "Current index is greater than length" |
| 15015 | 0x3AA7 | `InvalidEnumValue` | "Invalid enum value" |
| 15016 | 0x3AA8 | `InsufficientCapacity` | "Insufficient capacity for operation" |
| 15017 | 0x3AA9 | `PlatformSizeOverflow` | "Value too large for platform usize" |
## 16001 - 16034 / LightSdkError Variants
Source code:
[https://github.com/Lightprotocol/light-protocol/blob/main/sdk-libs/sdk/src/error.rs](https://github.com/Lightprotocol/light-protocol/blob/main/sdk-libs/sdk/src/error.rs#L126)
| Code | Hex | Error | Message |
| :---- | :----- | :----------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ |
| 16001 | 0x3E81 | `ConstraintViolation` | "Constraint violation" |
| 16002 | 0x3E82 | `InvalidLightSystemProgram` | "Invalid light-system-program ID" |
| 16003 | 0x3E83 | `ExpectedAccounts` | "Expected accounts in the instruction" |
| 16004 | 0x3E84 | `ExpectedAddressTreeInfo` | "Expected address Merkle context to be provided" |
| 16005 | 0x3E85 | `ExpectedAddressRootIndex` | "Expected address root index to be provided" |
| 16006 | 0x3E86 | `ExpectedData` | "Accounts with a specified input are expected to have data" |
| 16007 | 0x3E87 | `ExpectedDiscriminator` | "Accounts with specified data are expected to have a discriminator" |
| 16008 | 0x3E88 | `ExpectedHash` | "Accounts with specified data are expected to have a hash" |
| 16009 | 0x3E89 | `ExpectedLightSystemAccount(String)` | "Expected the `{0}` light account to be provided" |
| 16010 | 0x3E8A | `ExpectedMerkleContext` | "`mut` and `close` accounts are expected to have a Merkle context" |
| 16011 | 0x3E8B | `ExpectedRootIndex` | "Expected root index to be provided" |
| 16012 | 0x3E8C | `TransferFromNoInput` | "Cannot transfer lamports from an account without input" |
| 16013 | 0x3E8D | `TransferFromNoLamports` | "Cannot transfer from an account without lamports" |
| 16014 | 0x3E8E | `TransferFromInsufficientLamports` | "Account, from which a transfer was attempted, has insufficient amount of lamports" |
| 16015 | 0x3E8F | `TransferIntegerOverflow` | "Integer overflow resulting from too large resulting amount" |
| 16016 | 0x3E90 | `Borsh` | "Borsh error." |
| 16017 | 0x3E91 | `FewerAccountsThanSystemAccounts` | "Fewer accounts than number of system accounts." |
| 16018 | 0x3E92 | `InvalidCpiSignerAccount` | "InvalidCpiSignerAccount" |
| 16019 | 0x3E93 | `MissingField(String)` | "Missing meta field: " |
| 16020 | 0x3E94 | `OutputStateTreeIndexIsNone` | "Output state tree index is none. Use an CompressedAccountMeta type with output tree index to initialize or update accounts." |
| 16021 | 0x3E95 | `InitAddressIsNone` | "Address is none during initialization" |
| 16022 | 0x3E96 | `InitWithAddressIsNone` | "Address is none during initialization with address" |
| 16023 | 0x3E97 | `InitWithAddressOutputIsNone` | "Output is none during initialization with address" |
| 16024 | 0x3E98 | `MetaMutAddressIsNone` | "Address is none during meta mutation" |
| 16025 | 0x3E99 | `MetaMutInputIsNone` | "Input is none during meta mutation" |
| 16026 | 0x3E9A | `MetaMutOutputLamportsIsNone` | "Output lamports is none during meta mutation" |
| 16027 | 0x3E9B | `MetaMutOutputIsNone` | "Output is none during meta mutation" |
| 16028 | 0x3E9C | `MetaCloseAddressIsNone` | "Address is none during meta close" |
| 16029 | 0x3E9D | `MetaCloseInputIsNone` | "Input is none during meta close" |
| 16031 | 0x3E9F | `CpiAccountsIndexOutOfBounds(usize)` | "CPI accounts index out of bounds: " |
| 16032 | 0x3EA0 | `InvalidCpiContextAccount` | "Invalid CPI context account" |
| 16033 | 0x3EA1 | `InvalidSolPoolPdaAccount` | "Invalid SolPool PDA account" |
| 16034 | 0x3EA2 | `InvalidCpiAccountsOffset` | "CpiAccounts accounts slice starts with an invalid account. It should start with LightSystemProgram SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7." |
***
## Didn't find what you were looking for?
Reach out! [Telegram](https://t.me/swen_light) | [email](mailto:support@lightprotocol.com) | [Discord](https://discord.com/invite/7cJ8BhAXhu)
# Debug ProofVerificationFailed (0x179b)
Source: https://www.zkcompression.com/resources/error-cheatsheet/debug-0x179b-6043-proofverificationfailed
Common cause and debug steps for ProofVerificationFailed (0x179B / 6043)
### Description
You're passing an invalid proof. The proof provided cannot be verified against the expected state.
For help with debugging use the [MCP Configuration](/ai-tools/mcp) or AskDevin via
### Common causes and Debug Steps
For a complete example of proper client+on chain flows, see the [Counter Program](https://github.com/Lightprotocol/program-examples/blob/main/counter/anchor/programs/counter/src/lib.rs#L26).
Compare client vs on chain seeds/addresses. Both should be identical.
```typescript theme={null}
// Client - log seeds/address used to request proof
console.log("Client seeds:", seeds, "address:", address);
```
```rust theme={null}
// Onchain - log seeds/address
msg!("Program seeds: {:?}, address: {:?}", seeds, address);
```
### Still having issues? We're here to help!
* Reach out on [Discord](https://discord.com/invite/CYvjBgzRFP) for support
* Share the exact error code and a reproducer (GitHub repo or [Gist](https://gist.github.com/))
# Guide to Migrate from v1 to v2 Merkle trees
Source: https://www.zkcompression.com/resources/migration-v1-to-v2
V2 reduces CU consumption by up to 70%. V1 remains supported for existing deployments.
## V2 Improvements
| | v1 | v2 |
| --------------------- | --------------------- | ----------------- |
| CU consumption | Baseline | Up to 70% less |
| Merkle tree type | Concurrent | Batched |
| State tree depth | 26 (\~67M leaves) | 32 (\~4B leaves) |
| Address tree depth | 26 | 40 |
| Address tree accounts | Separate tree + queue | Single batch tree |
### Update Cargo.toml
In 0.17.x onward, `v2` is a default feature of `light-sdk`. Make sure it stays enabled.
When you specify `features = [...]` in Cargo.toml, Cargo **disables default features**.
If you need extra features like `keccak` or `anchor`, either keep defaults explicitly
or add `v2` to your features list:
```toml theme={null}
# This DISABLES v2 (default features are off):
light-sdk = { version = "0.17.1", features = ["keccak"] }
# Either of these keeps v2 enabled:
light-sdk = { version = "0.17.1", features = ["keccak", "v2"] }
light-sdk = { version = "0.17.1", default-features = true, features = ["keccak"] }
```
Without `v2`, `add_system_accounts_v2` and `cpi::v2` are not available.
**On-chain program:**
```toml theme={null}
[dependencies]
light-sdk = "0.17.1"
light-hasher = "5.0.0"
```
For Anchor programs, add the `anchor` feature:
```toml theme={null}
[dependencies]
light-sdk = { version = "0.17.1", features = ["anchor"] }
```
**Off-chain client:**
```toml theme={null}
[dependencies]
light-client = { version = "0.17.2", features = ["v2"] }
```
### Update imports
```rust theme={null}
// v1
use light_sdk::{
address::v1::derive_address,
constants::ADDRESS_TREE_V1,
cpi::v1::{CpiAccounts, LightSystemProgramCpi},
};
```
```rust theme={null}
// v2
use light_sdk::{
address::v2::derive_address,
cpi::v2::{CpiAccounts, LightSystemProgramCpi},
constants::ADDRESS_TREE_V2,
};
```
### Update address derivation
The `derive_address` function signature remains the same, but the internal derivation logic differs. No code changes needed beyond updating the import path.
### Update address params
Replace `into_new_address_params_packed()` with `into_new_address_params_assigned_packed()`:
```rust theme={null}
// v1
let new_address_params = instruction_data
.address_tree_info
.into_new_address_params_packed(address_seed);
```
```rust theme={null}
// v2
let new_address_params = instruction_data
.address_tree_info
.into_new_address_params_assigned_packed(address_seed, Some(0));
```
The second parameter specifies the output queue index. Use `Some(0)` to assign the address to the first available queue.
### Update tree validation
If your program validates the address tree pubkey, update the constant:
```rust theme={null}
// v1
if address_tree_pubkey.to_bytes() != ADDRESS_TREE_V1 {
return Err(ProgramError::InvalidAccountData);
}
```
```rust theme={null}
// v2
if address_tree_pubkey.to_bytes() != ADDRESS_TREE_V2 {
return Err(ProgramError::InvalidAccountData);
}
```
### Update client-side account packing
```rust theme={null}
use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig};
let config = SystemAccountMetaConfig::new(YOUR_PROGRAM_ID);
let mut packed = PackedAccounts::default();
```
```rust theme={null}
// v1
packed.add_system_accounts(config)?;
```
```rust theme={null}
// v2
packed.add_system_accounts_v2(config)?;
```
**Full v2 client example**
```rust theme={null}
use light_client::rpc::{LightClient, LightClientConfig, Rpc, Indexer};
use light_sdk::{
address::v2::derive_address,
instruction::{PackedAccounts, SystemAccountMetaConfig},
};
// 1. Build PackedAccounts with v2 system accounts
let config = SystemAccountMetaConfig::new(YOUR_PROGRAM_ID);
let mut packed = PackedAccounts::default();
packed.add_system_accounts_v2(config)?;
// 2. Fetch validity proof
let address_tree = rpc.get_address_tree_v2();
let (address, _) = derive_address(&[b"my_seed", &id], &address_tree.tree, &YOUR_PROGRAM_ID);
let rpc_result = rpc
.get_validity_proof(
vec![],
vec![AddressWithTree { address, tree: address_tree.tree }],
None,
)
.await?
.value;
// 3. Pack tree infos into remaining accounts
let tree_infos = rpc_result.pack_tree_infos(&mut packed);
let address_tree_info = tree_infos.address_trees[0];
// 4. Pack output state tree
let state_tree = rpc.get_random_state_tree_info()?;
let output_state_tree_index = state_tree.pack_output_tree_index(&mut packed)?;
// 5. Convert to account metas for the instruction
let (remaining_accounts, _system_offset, _packed_offset) = packed.to_account_metas();
```
### Update client code (light-client)
```rust theme={null}
use light_client::rpc::{LightClient, LightClientConfig, Rpc};
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=YOUR_KEY".to_string();
let photon_url = "https://mainnet.helius-rpc.com".to_string();
let api_key = "YOUR_KEY".to_string();
let config = LightClientConfig::new(rpc_url, Some(photon_url), Some(api_key));
let mut rpc = LightClient::new(config).await?;
```
**Tree info methods:**
```rust theme={null}
// v1
let address_tree = rpc.get_address_tree_v1();
let state_tree = rpc.get_random_state_tree_info_v1()?;
```
```rust theme={null}
// v2
let address_tree = rpc.get_address_tree_v2();
let state_tree = rpc.get_random_state_tree_info()?; // returns v2 when feature enabled
```
**Validity proofs:**
```rust theme={null}
// get_validity_proof automatically uses v2 endpoint (default)
let proof_result = rpc
.get_validity_proof(hashes, addresses_with_trees, None)
.await?;
```
### Update imports
```typescript theme={null}
// v1
import {
deriveAddress,
deriveAddressSeed,
defaultTestStateTreeAccounts,
PackedAccounts,
} from "@lightprotocol/stateless.js";
```
```typescript theme={null}
// v2
import {
deriveAddressV2,
deriveAddressSeedV2,
batchAddressTree,
PackedAccounts,
} from "@lightprotocol/stateless.js";
```
### V2 mode (TypeScript)
Published **`@lightprotocol/stateless.js@^0.23.0`** defaults to **V2** — you do **not** need to set **`LIGHT_PROTOCOL_VERSION=V2`** in application code.
If you **build `stateless.js` from source** and need V1 artifacts, set **`LIGHT_PROTOCOL_VERSION=V1`** for that build. For rare runtime overrides in custom bundles, you can still assign **`featureFlags.version`** before other imports run (advanced; prefer matching published versions instead).
### Update address derivation
The seed and address derivation functions have different signatures in v2:
```typescript theme={null}
// v1 - Program ID passed to seed derivation
const seed = deriveAddressSeed(
[counterSeed, signer.publicKey.toBytes()],
new PublicKey(program.idl.address)
);
const address = deriveAddress(seed, addressTree);
```
```typescript theme={null}
// v2 - Program ID passed to address derivation
const seed = deriveAddressSeedV2([counterSeed, signer.publicKey.toBytes()]);
const address = deriveAddressV2(
seed,
addressTree,
new PublicKey(program.idl.address)
);
```
### Update address tree references
Use `batchAddressTree` instead of `defaultTestStateTreeAccounts().addressTree`:
```typescript theme={null}
// v1
const addressTree = defaultTestStateTreeAccounts().addressTree;
const addressQueue = defaultTestStateTreeAccounts().addressQueue;
// In proof request
{
tree: addressTree,
queue: addressQueue,
address: bn(address.toBytes()),
}
```
```typescript theme={null}
// v2 - queue equals tree for batch trees
const addressTree = new PublicKey(batchAddressTree);
// In proof request
{
tree: addressTree,
queue: addressTree,
address: bn(address.toBytes()),
}
```
### Update PackedAccounts
Use the v2 variant when building remaining accounts:
```typescript theme={null}
// v1
const remainingAccounts = PackedAccounts.newWithSystemAccounts(systemAccountConfig);
```
```typescript theme={null}
// v2
const remainingAccounts = PackedAccounts.newWithSystemAccountsV2(systemAccountConfig);
```
```text theme={null}
---
argument-hint:
description: Migrate Light Protocol program from v1 to v2 Merkle trees
allowed-tools: [Bash, Read, Glob, Grep, Task, WebFetch]
---
Migrate this Light Protocol program from v1 to v2 Merkle trees.
## Goal
Produce a **fully working migration** that builds and tests pass.
## Available commands
Via Bash tool:
- **cargo build-sbf**, **cargo test-sbf**, **cargo fmt**, **cargo clippy**
- **anchor build**, **anchor test**
- **grep**, **sed**
## Documentation
- Migration Guide: https://zkcompression.com/references/migration-v1-to-v2
- Reference PR: https://github.com/Lightprotocol/program-examples/commit/54f0e7f15c2972a078f776cfb40b238d83c7e486
## Reference repos
program-examples/counter/anchor/
├── programs/counter/src/lib.rs # v2 patterns: derive_address, CpiAccounts
├── Cargo.toml # v2 feature flags
└── tests/counter.ts # v2 client patterns
## Workflow
### Phase 1: Index program
Find all v1 patterns:
grep -r "::v1::" src/ tests/
grep -r "ADDRESS_TREE_V1" src/
grep -r "into_new_address_params_packed" src/
grep -r "get_address_tree_v1" tests/
### Phase 2: Update dependencies
Update Cargo.toml. V2 is the default - no feature flag needed:
# On-chain program
[dependencies]
light-sdk = { version = "0.17.1", features = ["anchor"] }
light-hasher = "5.0.0"
# Off-chain client
[dependencies]
light-client = "0.17.2"
Note: V2 is now the default in all crates. Only specify `features = ["v2"]` if you disabled default features.
### Phase 3: Rust SDK replacements
| v1 Pattern | v2 Replacement |
|------------|----------------|
| address::v1::derive_address | address::v2::derive_address |
| cpi::v1::CpiAccounts | cpi::v2::CpiAccounts |
| cpi::v1::LightSystemProgramCpi | cpi::v2::LightSystemProgramCpi |
| constants::ADDRESS_TREE_V1 | constants::ADDRESS_TREE_V2 |
| .into_new_address_params_packed(seed) | .into_new_address_params_assigned_packed(seed, Some(0)) |
| .add_system_accounts(config) | .add_system_accounts_v2(config) |
### Phase 4: TypeScript SDK replacements
| v1 Pattern | v2 Replacement |
|------------|----------------|
| deriveAddress( | deriveAddressV2( |
| deriveAddressSeed( | deriveAddressSeedV2( |
| defaultTestStateTreeAccounts().addressTree | batchAddressTree |
| .newWithSystemAccounts( | .newWithSystemAccountsV2( |
| get_address_tree_v1() | get_address_tree_v2() |
| get_random_state_tree_info_v1() | get_random_state_tree_info() |
### Phase 5: Build and test loop
**Required commands (no shortcuts):**
For Anchor programs: **anchor build && anchor test**
For Native programs: **cargo build-sbf && cargo test-sbf**
**NO shortcuts allowed:**
- Do NOT use **cargo build** (must use **cargo build-sbf**)
- Do NOT use **cargo test** (must use **cargo test-sbf**)
- Tests MUST run against real BPF bytecode
**On failure:** Spawn debugger agent with error context.
**Loop rules:**
1. Each debugger gets fresh context + previous debug reports
2. Each attempt tries something DIFFERENT
3. **NEVER GIVE UP** - keep spawning until fixed
Do NOT proceed until all tests pass.
## DeepWiki fallback
If no matching pattern in reference repos:
mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "How to migrate {pattern} from v1 to v2?")
```
## View Program Examples
***
## Didn't find what you were looking for?
Reach out! [Telegram](https://t.me/swen_light) | [email](mailto:support@lightprotocol.com) | [Discord](https://discord.com/invite/7cJ8BhAXhu)
# Security
Source: https://www.zkcompression.com/resources/security
Overview to Light Protocol's bug bounty program, third party security audits, and formal verification of circuits.
### Bug Bounty
Light Protocol is hosting a [bug bounty program](https://immunefi.com/bug-bounty/light-protocol/information/).
### Security Audits
The Light protocol on-chain programs were audited by independent security firms Certora, OtterSec, Accretion, HashCloak, Neodyme, and Zellic.
| Firm | Scope | Date | Report |
| :------------ | :----------------------------------- | :------------ | :------------------------------------------------------------------------------------------------------------------------------------------ |
| **Certora** | Light Token | December '25 | [View](https://github.com/Lightprotocol/light-protocol/blob/main/audits/certora_2025-12_light-token.pdf) |
| **OtterSec** | CPI Context Refactor | December '25 | [View](https://github.com/Lightprotocol/light-protocol/blob/main/audits/ottersec_2025-12_cpi-context-refactor.pdf) |
| **Accretion** | ZK Compression v2 | June '25 | [View](https://github.com/Lightprotocol/light-protocol/blob/main/audits/accretion_2025-06_zkcompression-v2.pdf) |
| **OtterSec** | Batched Merkle Trees | June '25 | [View](https://github.com/Lightprotocol/light-protocol/blob/main/audits/ottersec_2025-06_batched-merkle-trees.pdf) |
| **HashCloak** | Compressed Token & ZK Compression v2 | March '25 | [View](https://github.com/Lightprotocol/light-protocol/blob/main/audits/hashcloak_2025-03_compressed-token-update-and-zkcompression-v2.pdf) |
| **OtterSec** | Zerocopy | March '25 | [View](https://github.com/Lightprotocol/light-protocol/blob/main/audits/ottersec_2025-03_zerocopy.pdf) |
| **Accretion** | Compressed Token Program Update | January '25 | [View](https://github.com/Lightprotocol/light-protocol/blob/main/audits/accretion_2025-01_compressed-token-program-update.pdf) |
| **Zellic** | ZK Compression v1 | September '24 | [View](https://github.com/Lightprotocol/light-protocol/blob/main/audits/zellic_2024-09_zkcompression-v1.pdf) |
| **Neodyme** | ZK Compression v1 | August '24 | [View](https://github.com/Lightprotocol/light-protocol/blob/main/audits/neodyme_2024-08_zkcompression-v1.pdf) |
| **OtterSec** | ZK Compression v1 | August '24 | [View](https://github.com/Lightprotocol/light-protocol/blob/main/audits/ottersec_2024-08_zkcompression-v1.pdf) |
### Groth16 Circuit Security
The ZK Compression circuit was formally verified by Reilabs. See the report [here](https://github.com/Lightprotocol/light-protocol/blob/main/audits/reilabs_2024-08_circuits_formal_verification_report.pdf).
Information about the Trusted Setup Ceremony for the groth16 circuits is [here](https://github.com/Lightprotocol/gnark-mt-setup/blob/main/README.md).
**For additional information on Light Protocol's security policy, read** [**here**](https://github.com/Lightprotocol/light-protocol/blob/main/SECURITY.md)**.**
# Support
Source: https://www.zkcompression.com/support
Get expert help with Compressed Tokens and ZK Compression. Discord community, Telegram, and Email support available.
Head to Light's Discord.
Head to the Helius Discord.
## Reporting Technical Issues
Before reaching out, try the Error Cheatsheet or DeepWiki.
When reporting technical issues,
1. Include as much detail as possible in your question,
2. Provide a reproducer or [GitHub code snippet](https://gist.github.com/) if possible,
3. Please use text (not screenshots) to show error messages so other people with the same problem can find your question.
## Quick Fixes
### Look Up Errors
### AI Assistance
Install our MCP server for advanced AI assistance.
#### Alternative: DeepWiki Web
If you don't have the MCP installed we recommend to use AskDevin in your browser.
You can index and query any public GitHub repository with DeepWiki to produce wikis with architecture diagrams, source links, and codebase summaries.
# Examples
Source: https://www.zkcompression.com/zk/examples
Example projects for building ZK programs on Solana.
Use the [zk-nullifier](https://github.com/Lightprotocol/skills/tree/main/skills/zk-nullifier) agent skill to build privacy-preserving ZK programs with nullifiers:
```
npx skills add Lightprotocol/skills
```
For orchestration, install the [general skill](https://zkcompression.com/skill.md):
```bash theme={null}
npx skills add https://zkcompression.com
```
| | Description |
| :--------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------ |
| [ZK-ID](https://github.com/Lightprotocol/program-examples/tree/main/zk/zk-id) | Identity verification using Groth16 proofs. Issuers create credentials; users prove ownership without revealing the credential. |
| [Nullifier](https://github.com/Lightprotocol/program-examples/tree/main/zk/zk-nullifier) | Simple Program to Create Nullifiers. |
# Primitives for ZK on Solana
Source: https://www.zkcompression.com/zk/overview
Overview how to build a ZK program on Solana.
***
Use the [zk-nullifier](https://github.com/Lightprotocol/skills/tree/main/skills/zk-nullifier) agent skill to build privacy-preserving ZK programs with nullifiers:
```
npx skills add Lightprotocol/skills
```
For orchestration, install the [general skill](https://zkcompression.com/skill.md):
```bash theme={null}
npx skills add https://zkcompression.com
```
Building a ZK Solana program requires:
1. Nullifiers to prevent double spending
2. Proof verification
3. A Merkle tree to store state
4. An indexer to serve Merkle proofs
5. Encrypted state
## Nullifiers for ZK on Solana
A nullifier is a deterministically derived hash to ensure an action can only be performed once.
The nullifier cannot be linked to the action or user.
For example Zcash uses nullifiers to prevent double spending.
To implement nullifiers we need a data structure that ensures every nullifier is only created once and never deleted.
On Solana a straightforward way to implement nullifiers is to create a PDA account with the nullifier as seed.
* PDA accounts cannot be closed and permanently lock 890,880 lamports (per nullifier rent-exemption).
* Compressed PDAs are derived similar to Solana PDAs and cost 15,000 lamports to create (no rent-exemption).
| Storage | Cost per nullifier |
| -------------- | ------------------ |
| PDA | 890,880 lamports |
| Compressed PDA | 15,000 lamports |
[See full example with tests on GitHub](https://github.com/Lightprotocol/program-examples/tree/main/zk/nullifier).
```rust theme={null}
// add to your program
use anchor_lang::prelude::*;
use nullifier_creation::{create_nullifiers, NullifierInstructionData};
declare_id!("Bw8aty8LJY5Kg2b6djghjWGwt6cBc1tVQUoreUehvVq4");
#[program]
pub mod zk_nullifier {
use super::*;
pub fn create_nullifier<'info>(
ctx: Context<'_, '_, '_, 'info, CreateNullifierAccounts<'info>>,
data: NullifierInstructionData,
nullifiers: Vec<[u8; 32]>,
) -> Result<()> {
// Verify your proof here. Use nullifiers as public inputs
// among your other public inputs.
// Example:
// let public_inputs = [...nullifiers, ...your_other_inputs];
// Groth16Verifier::new(...).verify()?;
create_nullifiers(
&nullifiers,
data,
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
)
}
}
#[derive(Accounts)]
pub struct CreateNullifierAccounts<'info> {
#[account(mut)]
pub signer: Signer<'info>,
}
```
## Groth16 Proof Verification on Solana
Groth16's small proof size and fast verification (\~200k compute units) make it the practical choice for Solana.
Find more information on [docs.rs](https://docs.rs/groth16-solana) and [GitHub](https://github.com/Lightprotocol/groth16-solana).
```rust theme={null}
let mut public_inputs_vec = Vec::new();
for input in PUBLIC_INPUTS.chunks(32) {
public_inputs_vec.push(input);
}
let proof_a: G1 =
::read(&*[&change_endianness(&PROOF[0..64])[..], &[0u8][..]].concat())
.unwrap();
let mut proof_a_neg = [0u8; 65];
::write(&proof_a.neg(), &mut proof_a_neg[..]).unwrap();
let proof_a = change_endianness(&proof_a_neg[..64]).try_into().unwrap();
let proof_b = PROOF[64..192].try_into().unwrap();
let proof_c = PROOF[192..256].try_into().unwrap();
let mut verifier = Groth16Verifier::new(
&proof_a,
&proof_b,
&proof_c,
public_inputs_vec.as_slice(),
&VERIFYING_KEY,
)
.unwrap();
verifier.verify().unwrap();
```
## Merklelized State with Indexer Support
ZK applications on Solana can use existing state Merkle trees to store state in rent-free accounts.
* This way you don't need to maintain your own Merkle tree and indexer.
* RPCs that support ZK Compression (Helius, Triton) index state changes.
| Creation Cost | Regular PDA | Compressed PDA |
| :------------------- | -----------: | -------------: |
| **100-byte account** | \~0.0016 SOL | 0.000015 SOL |
Your circuit must include compressed accounts. Find [guides to compressed accounts in the documentation](/compressed-pdas/overview) and the [full example with zk implementation here](https://github.com/Lightprotocol/program-examples/blob/99d260f9f356743b8fe3501c684f7926930d6079/zk-id/circuits/compressed_account.circom).
## Get Started & Examples
| | Description |
| :--------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------ |
| [ZK-ID](https://github.com/Lightprotocol/program-examples/tree/main/zk/zk-id) | Identity verification using Groth16 proofs. Issuers create credentials; users prove ownership without revealing the credential. |
| [Nullifier](https://github.com/Lightprotocol/program-examples/tree/main/zk/zk-nullifier) | Simple Program to Create Nullifiers. |