Skip to content
Merged
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
208 changes: 208 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
# CLAUDE.md

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

## Project overview

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.

## Build and test commands

### TypeScript client

```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

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

### Privy Node.js (devnet)

```bash
cd 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 balances # Query balance breakdown (hot, cold, SPL/T22, SOL)
npm run history # Transaction history for light-token interface ops

# Setup helpers live in 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
```

### Privy React (devnet — WIP)

```bash
cd 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
```

### 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
```

## Architecture

### Workspace layout

Root `package.json` defines npm workspaces: `typescript-client`, `toolkits/payments-and-wallets`, `privy/nodejs`, `privy/react`, and `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
- `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 (`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**.

**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>`.

**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.

**Two workspaces, two signing modes**:

- **App operations** (`privy/nodejs/src/*.ts`): Privy server wallet signing. Six scripts: `transfer`, `wrap`, `unwrap`, `load`, `balances`, `get-transaction-history`.
- **Setup helpers** (`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`.

**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).

**Per-script details**:

- **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`.
- **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.

**Import subpaths**: Most light-token instructions come from `@lightprotocol/compressed-token/unified`. Exception: `getSplInterfaceInfos` comes from root `@lightprotocol/compressed-token`.

### Key dependencies

- `@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

## Environment setup

Copy `.env.example` to `.env` and set `API_KEY` for devnet/mainnet RPC access. Localnet uses default endpoints (no env needed).

## Documentation

https://www.zkcompression.com/light-token/welcome
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
"private": true,
"workspaces": [
"typescript-client",
"toolkits/payments-and-wallets"
"toolkits/payments-and-wallets",
"privy/react",
"privy/nodejs",
"privy/scripts"
],
"scripts": {
"toolkit:payments": "npm run -w toolkits/payments-and-wallets"
},
"dependencies": {
"@lightprotocol/compressed-token": "^0.23.0-beta.8",
"@lightprotocol/stateless.js": "^0.23.0-beta.8",
"@lightprotocol/compressed-token": "beta",
"@lightprotocol/stateless.js": "beta",
"@solana/spl-token": "^0.4.13",
"@solana/web3.js": "1.98.4",
"dotenv": "^16.5.0"
Expand Down
142 changes: 142 additions & 0 deletions privy/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# CLAUDE.md

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

## Project overview

Two example apps demonstrating Privy wallet integration with Light Token on Solana devnet. Both apps show transfer, wrap, unwrap, load, and balance query flows using Privy-managed wallets for transaction signing.

## Sub-projects

### `nodejs/` — Server-side (Node.js)

Backend scripts using `@privy-io/node` with server-side wallet signing via `TREASURY_AUTHORIZATION_KEY`. Each script is a standalone operation run with `tsx`.

### `react/` — Client-side (React + Vite) — WIP

Browser app using `@privy-io/react-auth` with client-side wallet signing via `useSignTransaction`. Privy creates embedded Solana wallets on login. Vite dev server with Tailwind CSS v4 and `vite-plugin-node-polyfills` for Buffer.

## Build and run

### Node.js scripts

```bash
cd nodejs
npm install
cp .env.example .env # fill in all values

# 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 balances # Query balance breakdown (hot, cold, SPL/T22, SOL)
npm run history # Transaction history for light-token interface ops

# Setup helpers (use local filesystem keypair, not Privy)
npm run mint:spl-and-wrap # Create mint + interface PDA + fund treasury
npm run mint:spl # Mint SPL/T22 tokens to existing mint
npm run register:spl-interface # Register interface PDA on existing mint
npm run decompress # Decompress light-token ATA to T22 ATA
```

### React app

```bash
cd react
npm install
cp .env.example .env # fill in VITE_PRIVY_APP_ID and VITE_HELIUS_RPC_URL

npm run dev # start Vite dev server
npm run build # production build
```

## Architecture

### Privy signing pattern

Both apps follow the same flow: build an unsigned `Transaction`, serialize with `requireAllSignatures: false`, sign via Privy, deserialize, and send with `sendRawTransaction`.

**Node.js** — signs via `privy.wallets().solana().signTransaction(walletId, { transaction, authorization_context })`. Requires `TREASURY_WALLET_ID` and `TREASURY_AUTHORIZATION_KEY`.

**React** — signs via `useSignTransaction` hook: `signTransaction({ transaction, wallet, chain: 'solana:devnet' })`. Privy handles embedded wallet key management client-side.

### Node.js modules (`nodejs/src/`)

Standalone async functions, each creating their own `PrivyClient` and `createRpc`:

- `transfer.ts` — `createTransferInterfaceInstruction`, auto-loads cold balance via `createLoadAtaInstructionsFromInterface`
- `wrap.ts` — `createWrapInstruction` with SPL interface lookup via `getSplInterfaceInfos`
- `unwrap.ts` — `createUnwrapInstruction` from `@lightprotocol/compressed-token/unified`
- `load.ts` — `createLoadAtaInstructionsFromInterface` to consolidate cold + SPL + T22 into light-token ATA
- `balances.ts` — queries hot (`getAtaInterface`), cold (`getCompressedTokenBalancesByOwnerV2`), SPL T22 (`getTokenAccountsByOwner` + raw data parsing)
- `get-transaction-history.ts` — `getSignaturesForOwnerInterface`
- `config.ts` — centralized env var exports with validation

**Setup helpers** (`nodejs/src/helpers/`):

- `mint-spl-and-wrap.ts` — `createMintInterface` + mint + wrap + transfer to treasury (filesystem wallet)
- `mint-spl.ts` — `createMintToInstruction` to existing mint (filesystem wallet)
- `register-spl-interface.ts` — `createSplInterface` on existing mint (filesystem wallet)
- `decompress.ts` — `decompressInterface` from light-token ATA to T22 ATA (filesystem wallet)

### Transaction routing (React `TransferForm`)

The `TransferForm` component routes actions based on `TokenBalance.isLightToken`:

- Light-token balance → `useTransfer` → `createTransferInterfaceInstruction`
- SPL balance → `useWrap` → `createWrapInstruction` (wraps to own light-token ATA)
- SOL → display only, no transfer action

### React hooks (`react/src/hooks/`)

Each hook returns `{ actionFn, isLoading }` and accepts `{ params, wallet, signTransaction }`:

- `useTransfer` — light-token ATA to ATA transfer
- `useWrap` — SPL to light-token (creates light-token ATA idempotently, verifies SPL balance)
- `useUnwrap` — light-token to SPL T22 (creates T22 ATA if missing)
- `useLightTokenBalances` — fetches SOL, SPL (Token Program), and light-token (T22) balances by parsing raw account data
- `useTransactionHistory` — queries `getSignaturesForOwnerInterface`

## Environment variables

### Node.js (`nodejs/.env`)

| Variable | Required | Purpose |
|---|---|---|
| `PRIVY_APP_ID` | Yes | Privy application ID |
| `PRIVY_APP_SECRET` | Yes | Privy server-side secret |
| `TREASURY_WALLET_ID` | Yes | Privy wallet ID for signing |
| `TREASURY_WALLET_ADDRESS` | Yes | Public key of treasury wallet |
| `TREASURY_AUTHORIZATION_KEY` | Yes | EC private key for ECDSA transaction authorization |
| `HELIUS_RPC_URL` | Yes | Helius RPC endpoint (devnet) |
| `TEST_MINT` | No | Token mint address for scripts |
| `DEFAULT_TEST_RECIPIENT` | No | Defaults to `TREASURY_WALLET_ADDRESS` |
| `DEFAULT_AMOUNT` | No | Defaults to `0.001` |
| `DEFAULT_DECIMALS` | No | Defaults to `9` |

### React (`react/.env`)

| Variable | Required | Purpose |
|---|---|---|
| `VITE_PRIVY_APP_ID` | Yes | Privy application ID |
| `VITE_HELIUS_RPC_URL` | Yes | Helius RPC endpoint (devnet) |

## Key dependencies

- `@privy-io/node` ^0.1.0-alpha.2 — server-side Privy SDK
- `@privy-io/react-auth` ^3.9.1 — client-side Privy SDK
- `@lightprotocol/compressed-token` beta — light-token instructions (also exports `/unified` subpath for unwrap, load, balances)
- `@lightprotocol/stateless.js` beta — RPC client (`createRpc`)
- `@solana/web3.js` 1.98.4 — Solana web3 v1
- `@solana/spl-token` ^0.4.13 — SPL token operations (T22 ATA creation, balance checks)
- `@solana/kit` ^5.5.1 — Solana RPC for Privy provider config (React only)

## Important patterns

- Wrap and unwrap require `ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 })`. Transfer does not.
- Wrap calls `getSplInterfaceInfos` to find the initialized SPL interface and its `tokenProgram`.
- Unwrap imports from `@lightprotocol/compressed-token/unified` (not the main export).
- Helper scripts use filesystem wallet (`~/.config/solana/id.json`), not Privy, because they need a keypair signer for mint authority.
- The React app derives WebSocket URL from RPC URL by replacing `https://` with `wss://`.
- Both apps target `solana:devnet` (CAIP-2 chain ID `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`).
Loading
Loading