Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 34 additions & 12 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
name: Setup Environment
description: Setup Rust, Solana CLI, and Light CLI for testing
description: Setup Rust, Solana CLI, and optionally Node.js for testing

inputs:
example:
description: "Example directory path"
required: true
solana-cli-version:
description: "Solana CLI version"
required: false
default: "2.1.21"
default: "2.3.11"
rust-toolchain:
description: "Rust toolchain version"
required: false
default: "1.85.0"
default: "1.90.0"
light-cli-version:
description: "Light CLI version"
required: false
default: "alpha"
photon-indexer:
description: "Install Photon indexer (required for TypeScript tests)"
required: false
default: "false"

runs:
using: composite
Expand All @@ -22,26 +29,27 @@ runs:
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ inputs.rust-toolchain }}
cache: false
cache-workspaces: ${{ inputs.example || '.' }}

- name: Setup Node.js (for Light CLI)
- name: Setup Node.js
if: inputs.node-version != ''
uses: actions/setup-node@v4
with:
node-version: "22"
node-version: ${{ inputs.node-version }}

- name: Cache npm global packages
uses: actions/cache@v4
- name: Setup Node.js (for Light CLI)
if: inputs.node-version == ''
uses: actions/setup-node@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-light-cli-${{ inputs.light-cli-version }}
node-version: "22"

- name: Cache Solana CLI tools
uses: actions/cache@v4
with:
path: |
~/.cache/solana/
~/.local/share/solana/
key: solana-cli-${{ runner.os }}-${{ inputs.solana-cli-version }}
key: solana-cli-${{ runner.os }}-build-${{ inputs.solana-cli-version }}

- name: Install Solana CLI tools
shell: bash
Expand All @@ -53,6 +61,21 @@ runs:
shell: bash
run: npm install -g @lightprotocol/zk-compression-cli@${{ inputs.light-cli-version }}

- name: Cache Photon indexer
if: inputs.photon-indexer == 'true'
id: cache-photon
uses: actions/cache@v4
with:
path: ~/.cargo/bin/photon
key: photon-${{ runner.os }}-ac7df6c388db847b7693a7a1cb766a7c9d7809b5

- name: Install Photon indexer
if: inputs.photon-indexer == 'true' && steps.cache-photon.outputs.cache-hit != 'true'
shell: bash
env:
RUSTFLAGS: "-A dead-code"
run: cargo install --git https://github.com/lightprotocol/photon.git --rev ac7df6c388db847b7693a7a1cb766a7c9d7809b5 --locked --force

- name: Generate keypair
shell: bash
run: solana-keygen new --no-bip39-passphrase
Expand All @@ -64,4 +87,3 @@ runs:
cargo --version
solana --version
light --version
5 changes: 5 additions & 0 deletions .github/workflows/rust-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ jobs:
- light-token-macro-create-mint
- light-token-macro-create-token-account
- create-and-transfer
# example programs
- escrow
- fundraiser
- light-token-minter
- swap_example
steps:
- uses: actions/checkout@v4

Expand Down
27 changes: 20 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
# Dependencies
**/node_modules/
**/dist/

# Environment and secrets
**/.env
**/.env.*
**/*-keypair.json
**/id.json

# Build artifacts
**/target
**/.anchor

# Test artifacts
**/test-ledger

# Lock files
**/pnpm-lock.yaml
**/package-lock.json

# Backups
**/*.json.bak
**/test-ledger
**/target
cli/accounts
README.html
README_files/

**/.surfpool
.surfpool
# Project-specific
cli/accounts/
bugs.md
225 changes: 46 additions & 179 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,209 +2,76 @@

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project overview
## What this repo is

Light Token examples repository demonstrating rent-free token operations on Solana. Light Token reduces mint and token account costs by 200x through sponsored rent-exemption.
Light Token example programs demonstrating rent-free token vaults using Light Protocol on Solana. Contains Anchor programs (escrow, fundraiser, token-swap, light-token-minter), a shared test utilities crate, TypeScript client examples, and toolkits (payments, streaming).

## Build and test commands
## Build and test

### TypeScript client
All Anchor programs live in `programs/anchor/` as a Cargo workspace. Build and test from that directory:

```bash
# Install dependencies (from repo root — npm workspaces)
npm install

# Run from typescript-client directory
cd typescript-client
npm run test:actions # Run all action examples (chained sequentially)
npm run test:instructions # Run all instruction examples (chained sequentially)

# Individual examples (pattern: <operation>:<level>)
npm run create-mint:action # Create light-token mint
npm run create-mint:instruction # Create mint with manual instruction building
npm run transfer-interface:action # Transfer between account types
npm run wrap:action # Wrap SPL/T22 to light-token
npm run unwrap:action # Unwrap light-token to SPL/T22
npm run load-ata:action # Load compressed tokens into light ATA
npm run delegate:approve # Approve delegate
npm run delegate:revoke # Revoke delegate

# All examples require a running validator — see "Local development" below
```

### Rust client

```bash
cd rust-client
cargo run --example action_create_mint
cargo run --example action_transfer_interface
cargo run --example instruction_transfer_interface
```

### Anchor programs

```bash
cd programs/anchor

anchor build

# Test single program (recommended — must use single thread)
cargo test-sbf -p <package-name> -- --test-threads=1
# Build a single program
cd programs/anchor && cargo build-sbf --manifest-path escrow/Cargo.toml

# Package names: light-token-anchor-transfer-interface, light-token-anchor-mint-to,
# counter, create-and-transfer
```

### Privy Node.js (devnet)

```bash
cd toolkits/sign-with-privy/nodejs
npm install
cp .env.example .env # fill in Privy credentials + Helius RPC URL

# App operations (Privy-signed server wallet)
npm run transfer # Light-token ATA → ATA transfer (auto-loads cold balance)
npm run wrap # SPL/T22 → light-token ATA
npm run unwrap # Light-token ATA → SPL/T22
npm run load # Consolidate cold + SPL + T22 into light-token ATA
npm run light-balance # Query Light Token balance (hot, cold, unified) for TEST_MINT
npm run balances # Query balance breakdown (hot, cold, SPL/T22, SOL)
npm run history # Transaction history for light-token interface ops

# Setup helpers live in toolkits/sign-with-privy/scripts/ (separate workspace, uses local keypair)
cd ../scripts
npm run mint:spl-and-wrap <recipient> # Create mint + interface PDA + fund treasury
npm run mint:spl <mint> <recipient> # Mint SPL/T22 tokens to existing mint
npm run register:spl-interface <mint> # Register interface PDA on existing mint
```
# Test a single program (runs LiteSVM-based tests, no validator needed)
cd programs/anchor && cargo test-sbf -p escrow
cd programs/anchor && cargo test-sbf -p fundraiser
cd programs/anchor && cargo test-sbf -p light-token-minter
cd programs/anchor && cargo test-sbf -p swap_example

### Privy React (devnet — WIP)
# Run a single test by name
cd programs/anchor && cargo test-sbf -p escrow -- test_escrow_spl

```bash
cd toolkits/sign-with-privy/react
npm install
cp .env.example .env # fill in VITE_PRIVY_APP_ID and VITE_HELIUS_RPC_URL

npm run dev # Vite dev server
npm run build # Production build
# TypeScript client examples
cd typescript-client && npm install && npm run create-mint:action
```

### Local development

```bash
# Start test validator with Light programs (requires light CLI)
light test-validator

# Validator health check
curl http://127.0.0.1:8784/health
```
CI runs `cargo test-sbf -p <package>` for each program (see `.github/workflows/rust-tests.yml`). Solana CLI 2.3.11, Rust 1.90.0.

## Architecture

### Workspace layout

Root `package.json` defines npm workspaces: `typescript-client`, `toolkits/payments-and-wallets`, `toolkits/sign-with-privy/nodejs`, `toolkits/sign-with-privy/react`, and `toolkits/sign-with-privy/scripts`. Dependencies are hoisted to root.

- `typescript-client/` — Core examples: `actions/` (high-level) and `instructions/` (low-level)
- `rust-client/` — Rust client examples as cargo examples
- `programs/anchor/` — On-chain Anchor programs
- `basic-macros/` — Declarative `#[light_account]` macro pattern
- `basic-instructions/` — Explicit CPI calls to light-token program
- `create-and-transfer/` — Combined macro + CPI example
- `toolkits/` — Domain-specific implementations
- `payments-and-wallets/` — Wallet integration patterns (uses `@lightprotocol/compressed-token/unified` subpath)
- `streaming-tokens/` — Laserstream-based token indexing
- `sign-with-privy/` — Privy wallet integration examples (devnet)
- `nodejs/` — Server-side scripts using `@privy-io/node` with server wallet signing
- `react/` — Browser app using `@privy-io/react-auth` with embedded wallet signing (WIP)

### TypeScript: actions vs instructions

Every example exists at two abstraction levels:

**Actions** (`typescript-client/actions/`): Call high-level SDK functions that build, sign, and send transactions in one call. Functions like `createMintInterface`, `transferInterface`, `wrap` return a transaction signature directly.

**Instructions** (`typescript-client/instructions/`): Build `TransactionInstruction` objects manually, assemble into `Transaction`, and call `sendAndConfirmTransaction`. Requires fetching tree info and validity proofs yourself:
- `getBatchAddressTreeInfo()` — address tree for new account creation
- `selectStateTreeInfo(await rpc.getStateTreeInfos())` — state tree selection
- `rpc.getValidityProofV2([], [{address, treeInfo}])` — ZK validity proof
- `ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 })` — required CU budget

### Boilerplate pattern

Every TypeScript example follows the same setup:

```typescript
import "dotenv/config";
import { createRpc } from "@lightprotocol/stateless.js";

// localnet (default — no args):
const rpc = createRpc();
// devnet:
// const rpc = createRpc(`https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`);

// Payer from local Solana keypair:
const payer = Keypair.fromSecretKey(
new Uint8Array(JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")))
);
```

### Token flow concepts

- **Light-token ATA**: Associated token account derived via `getAssociatedTokenAddressInterface(mint, owner)`. Created with `createAtaInterface` or `createAtaInterfaceIdempotent`.
- **Wrap**: SPL/T22 tokens → light-token ATA. Requires SPL interface PDA registered via `createSplInterface`.
- **Unwrap**: Light-token ATA → SPL/T22 tokens.
- **Load ATA**: Compressed tokens (cold storage) → light-token ATA (hot balance). Uses `loadAta` action or `createLoadAtaInstructions` + `buildAndSignTx`/`sendAndConfirmTx`.
- **Decompress**: Light-token ATA → SPL T22 account. Used as setup step before wrapping.

### Two on-chain patterns (Anchor programs)

**Macro pattern** (`basic-macros/`): Declarative `#[light_account(init)]` attribute and `LightAccounts` derive macro.

**CPI pattern** (`basic-instructions/`): Explicit CPI calls like `TransferInterfaceCpi`.

### Privy Node.js architecture (`toolkits/sign-with-privy/nodejs/`)

Server-side scripts that sign transactions via Privy's wallet API instead of a local keypair. Each script in `src/` is standalone and runnable with `tsx`. All scripts target **devnet**.
### Programs (`programs/anchor/`)

**Script pattern**: Each file exports a reusable async function AND has a `// --- main ---` block at the bottom that imports config values and runs it. Scripts are both importable as modules and directly runnable via `npm run <script>`.
All programs use `#[light_program]` + `#[program]` dual macros from Light Protocol. The `#[light_program]` macro generates `LightAccountVariant`, vault seed structs, and CPI signer boilerplate.

**Signing**: Each script inlines its own Privy signing — no shared utilities between scripts. Pattern: build `Transaction` → serialize with `requireAllSignatures: false``privy.wallets().solana().signTransaction(walletId, { transaction, authorization_context })``sendRawTransaction``confirmTransaction`. Scripts that receive `TransactionInstruction[][]` from the SDK loop over batches sequentially.
- **escrow** — Peer-to-peer token swap. `make_offer` creates an Offer account + rent-free Light vault; `take_offer` completes the swap and closes the vault. `Offer` derives `LightAccount` → generates `LightAccountVariant::Vault`, `VaultSeeds`, `OfferSeeds`.
- **fundraiser** — Crowdfunding. `initialize` creates a Fundraiser + vault; `contribute`, `check_contributions`, `refund`. `Fundraiser` uses `#[account]` only (no `LightAccount` derive) → no cold/hot vault loading possible.
- **token-swap** — AMM with `create_amm`, `create_pool`, `deposit_liquidity`, `withdraw_liquidity`, `swap_exact_tokens_for_tokens`, `create_pool_light_lp`.
- **light-token-minter** — Helper program to create Light mints with metadata and mint tokens.
- **shared-test-utils** — Reusable test helpers for SPL/T22/Light mint creation, ATA creation, SPL interface PDAs, balance verification. All programs depend on this for tests.

**Two workspaces, two signing modes**:
### Transfer routing (`shared.rs` in escrow/fundraiser)

- **App operations** (`toolkits/sign-with-privy/nodejs/src/*.ts`): Privy server wallet signing. Seven scripts: `transfer`, `wrap`, `unwrap`, `load`, `light-balance`, `balances`, `get-transaction-history`.
- **Setup helpers** (`toolkits/sign-with-privy/scripts/src/*.ts`): Separate npm workspace. Uses local filesystem keypair (`~/.config/solana/id.json`), not Privy. Scripts: `mint-spl-and-wrap`, `mint-spl`, `register-spl-interface`.
Each program's `shared.rs` contains `transfer_tokens()` which picks the CPI path at runtime:
- Both accounts Light → `TransferCheckedCpi` (direct, no interface PDA)
- One SPL/T22 + one Light → `TransferInterfaceCpi` (cross-standard, requires SPL interface PDA)

**Config**: All env vars centralized in `src/config.ts` with validation. Exports `TREASURY_WALLET_ID`, `TREASURY_WALLET_ADDRESS`, `TREASURY_AUTHORIZATION_KEY`, `HELIUS_RPC_URL`, `TEST_MINT`, and convenience defaults (`DEFAULT_TEST_RECIPIENT`, `DEFAULT_AMOUNT`, `DEFAULT_DECIMALS`). Scripts import from `./config.js` (ESM `.js` extension required).
### Token configurations tested

**Per-script details**:
Each program tests 5 token standard combinations via `TokenConfig` enum:
- `Spl` — SPL mint + SPL ATAs + Light vault
- `Token2022` — T22 mint + T22 ATAs + Light vault
- `Light` — Light mint + Light ATAs + Light vault
- `LightSpl` — SPL mint, but user accounts converted to Light ATAs before escrow
- `LightT22` — T22 mint, but user accounts converted to Light ATAs before escrow

- **transfer** — Calls `createTransferInterfaceInstructions` (from `/unified` subpath) which returns `TransactionInstruction[][]`. Auto-loads cold balance before transferring. Usually one tx.
- **wrap** — Manually assembles 3 instructions: `setComputeUnitLimit(200_000)` + `createAssociatedTokenAccountInterfaceIdempotentInstruction` + `createWrapInstruction`. Calls `getSplInterfaceInfos` (from root `@lightprotocol/compressed-token`, not `/unified`) to get `tokenProgram` and derive the correct SPL ATA.
- **unwrap** — Calls `createUnwrapInstructions` from `/unified` subpath. Returns batched instructions handling load + unwrap together.
- **load** — Calls `createLoadAtaInstructions`. Can return empty array (nothing to load). Derives light-token ATA via `getAssociatedTokenAddressInterface`.
- **light-balance** — Queries hot (`getAtaInterface`) and cold (`getCompressedTokenBalancesByOwnerV2`) for a single `TEST_MINT`. Returns hot, cold, unified.
- **balances** — Queries 4 sources: SOL (`getBalance`), hot (`getAtaInterface`), cold (`getCompressedTokenBalancesByOwnerV2`), SPL + T22 (raw `getTokenAccountsByOwner` with manual buffer parsing at offset 64). Hardcodes decimals=9.
- **get-transaction-history** — Calls `getSignaturesForOwnerInterface`, a light-token specific RPC method.
### Test setup pattern

**Import subpaths**: Most light-token instructions come from `@lightprotocol/compressed-token/unified`. Exception: `getSplInterfaceInfos` comes from root `@lightprotocol/compressed-token`.
Tests follow: `create_test_rpc()``setup_*_test(config)``create_token_account()`/`create_contributor()``run_*_full_flow()`. Common modules in `tests/common/mod.rs`.

### Key dependencies
### Cold/hot lifecycle (escrow only)

- `@lightprotocol/compressed-token` beta — TypeScript token client (also exports `/unified` subpath for unwrap, load, balances)
- `@lightprotocol/stateless.js` beta — TypeScript RPC client (`createRpc`, `buildAndSignTx`, `sendAndConfirmTx`)
- `@solana/web3.js` 1.98.x — Solana web3 (v1, not v2)
- `@solana/spl-token` 0.4.x — SPL token operations (used for T22 interop)
- `@privy-io/node` ^0.1.0-alpha.2 — Server-side Privy SDK (Node.js scripts)
- `light-sdk` 0.19.x — Rust core SDK
- `light-token` 0.4.x — Rust token operations
- Anchor 0.31.1, Solana SDK 2.2, Rust 1.90.0
Light accounts auto-compress after rent expires. Escrow tests exercise the full cycle: create → `warp_to_compress` (calls `rpc.warp_epoch_forward(30)`) → `load_light_accounts` → transact. Fundraiser skips this because `Fundraiser` state lacks `LightAccount` derive.

## Environment setup
### Local path dependencies

Copy `.env.example` to `.env` and set `API_KEY` for devnet/mainnet RPC access. Localnet uses default endpoints (no env needed).
`light-client` and `light-program-test` use local path deps pointing to `~/Workspace/light-protocol/`. The workspace `Cargo.toml` also patches all `light-*` crates to local paths to avoid version conflicts.

## Documentation
## Key SDK patterns

https://www.zkcompression.com/light-token/welcome
- `CpiSigner` + `derive_light_cpi_signer!` at crate root for each program
- Constants used in `token::owner_seeds` must be `pub use`-exported from `lib.rs`
- Per-program rent sponsor PDA: `derive_rent_sponsor_pda(&program_id)` — separate from global `LIGHT_TOKEN_RENT_SPONSOR`
- `get_create_accounts_proof()` fetches validity proofs for account creation (needed for `init` instructions, not reads)
- `InitializeRentFreeConfig` sets up per-program compression config before any `#[light_account(init)]`
Loading
Loading