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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Light token is a high-performance token standard that reduces the cost of mint a
| [Payments and Wallets](toolkits/payments-and-wallets/) | All you need for wallet integrations and payment flows. Minimal API differences to SPL. |
| [Streaming Tokens](toolkits/streaming-tokens/) | Stream mint events using Laserstream |
| [Sign with Privy](toolkits/sign-with-privy/) | Light-token operations signed with Privy wallets (Node.js + React) |
| [Sign with Wallet Adapter](toolkits/sign-with-wallet-adapter/) | Sign light-token transactions with Wallet Adapter (React) |
| [Sponsor Rent Top-Ups](toolkits/sponsor-rent-top-ups/) | Sponsor rent top-ups for users by setting your application as the fee payer |

## Client Examples
Expand Down
5 changes: 5 additions & 0 deletions toolkits/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ Light-token operations signed with [Privy](https://privy.io) wallets. Server-sid
- **[React](sign-with-privy/react/)** — Browser app using `@privy-io/react-auth` with embedded wallet signing
- **[Setup scripts](sign-with-privy/scripts/)** — Create test mints and fund wallets on devnet

### Sign with Wallet Adapter

Sign light-token transactions with [Wallet Adapter](https://github.com/anza-xyz/wallet-adapter). Transfer, wrap, unwrap, and balance queries.
- **[React](sign-with-wallet-adapter/react/)** — Browser app using `@solana/wallet-adapter-react` with Phantom, Backpack, Solflare, etc.

### Streaming Tokens

[Rust program example to stream mint events](streaming-tokens/) of the Light-Token Program.
Expand Down
74 changes: 45 additions & 29 deletions toolkits/sign-with-privy/nodejs/src/balances.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'dotenv/config';
import {PublicKey, LAMPORTS_PER_SOL} from '@solana/web3.js';
import {TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID} from '@solana/spl-token';
import {TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, getMint} from '@solana/spl-token';
import {createRpc} from '@lightprotocol/stateless.js';
import {
getAtaInterface,
Expand Down Expand Up @@ -36,13 +36,13 @@ export async function getBalances(
console.error('Failed to fetch SOL balance:', e);
}

// Per-mint accumulator
const mintMap = new Map<string, {spl: number; t22: number; hot: number; cold: number; decimals: number}>();
// Per-mint accumulator (raw values, converted at assembly)
const mintMap = new Map<string, {spl: bigint; t22: bigint; hot: bigint; cold: bigint; decimals: number; tokenProgram: PublicKey}>();

const getOrCreate = (mintStr: string) => {
let entry = mintMap.get(mintStr);
if (!entry) {
entry = {spl: 0, t22: 0, hot: 0, cold: 0, decimals: 9};
entry = {spl: 0n, t22: 0n, hot: 0n, cold: 0n, decimals: 9, tokenProgram: TOKEN_PROGRAM_ID};
mintMap.set(mintStr, entry);
}
return entry;
Expand All @@ -59,7 +59,7 @@ export async function getBalances(
const mint = new PublicKey(buf.subarray(0, 32));
const amount = buf.readBigUInt64LE(64);
const mintStr = mint.toBase58();
getOrCreate(mintStr).spl += toUiAmount(amount, 9);
getOrCreate(mintStr).spl += amount;
}
} catch {
// No SPL accounts
Expand All @@ -76,49 +76,66 @@ export async function getBalances(
const mint = new PublicKey(buf.subarray(0, 32));
const amount = buf.readBigUInt64LE(64);
const mintStr = mint.toBase58();
getOrCreate(mintStr).t22 += toUiAmount(amount, 9);
const entry = getOrCreate(mintStr);
entry.t22 += amount;
entry.tokenProgram = TOKEN_2022_PROGRAM_ID;
}
} catch {
// No Token 2022 accounts
}

// 3. Hot balance from Light Token associated token account
// 3. Cold balance from compressed token accounts
try {
const compressed = await rpc.getCompressedTokenBalancesByOwnerV2(owner);
for (const item of compressed.value.items) {
const mintStr = item.mint.toBase58();
getOrCreate(mintStr).cold += BigInt(item.balance.toString());
}
} catch {
// No compressed accounts
}

// 4. Fetch actual decimals for each mint
const mintKeys = [...mintMap.keys()];
await Promise.allSettled(
mintKeys.map(async (mintStr) => {
try {
const mint = new PublicKey(mintStr);
const entry = getOrCreate(mintStr);
const mintInfo = await getMint(rpc, mint, undefined, entry.tokenProgram);
entry.decimals = mintInfo.decimals;
} catch {
// Keep default decimals if mint fetch fails
}
}),
);

// 5. Hot balance from Light Token associated token account
await Promise.allSettled(
mintKeys.map(async (mintStr) => {
try {
const mint = new PublicKey(mintStr);
const ata = getAssociatedTokenAddressInterface(mint, owner);
const {parsed} = await getAtaInterface(rpc, ata, owner, mint);
getOrCreate(mintStr).hot = toUiAmount(parsed.amount, 9);
getOrCreate(mintStr).hot = BigInt(parsed.amount.toString());
} catch {
// Associated token account does not exist for this mint
}
}),
);

// 4. Cold balance from compressed token accounts
try {
const compressed = await rpc.getCompressedTokenBalancesByOwnerV2(owner);
for (const item of compressed.value.items) {
const mintStr = item.mint.toBase58();
getOrCreate(mintStr).cold += toUiAmount(BigInt(item.balance.toString()), 9);
}
} catch {
// No compressed accounts
}

// Assemble result
// 6. Assemble result (convert raw → UI amounts here)
const tokens: TokenBalance[] = [];
for (const [mintStr, entry] of mintMap) {
const d = entry.decimals;
tokens.push({
mint: mintStr,
decimals: entry.decimals,
hot: entry.hot,
cold: entry.cold,
spl: entry.spl,
t22: entry.t22,
unified: entry.hot + entry.cold,
decimals: d,
hot: toUiAmount(entry.hot, d),
cold: toUiAmount(entry.cold, d),
spl: toUiAmount(entry.spl, d),
t22: toUiAmount(entry.t22, d),
unified: toUiAmount(entry.hot + entry.cold, d),
});
}

Expand All @@ -131,9 +148,8 @@ function toBuffer(data: Buffer | Uint8Array | string | unknown): Buffer | null {
return null;
}

function toUiAmount(raw: bigint | {toNumber: () => number}, decimals: number): number {
const value = typeof raw === 'bigint' ? Number(raw) : raw.toNumber();
return value / 10 ** decimals;
function toUiAmount(raw: bigint, decimals: number): number {
return Number(raw) / 10 ** decimals;
}

export default getBalances;
Expand Down
2 changes: 1 addition & 1 deletion toolkits/sign-with-privy/react/src/hooks/useTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function useTransfer() {
const owner = new PublicKey(ownerPublicKey);
const mintPubkey = new PublicKey(mint);
const recipient = new PublicKey(toAddress);
const tokenAmount = Math.floor(amount * Math.pow(10, decimals));
const tokenAmount = Math.round(amount * Math.pow(10, decimals));

// Returns TransactionInstruction[][].
// Each inner array is one transaction.
Expand Down
50 changes: 33 additions & 17 deletions toolkits/sign-with-privy/react/src/hooks/useUnifiedBalance.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState, useCallback } from 'react';
import { PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';
import { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from '@solana/spl-token';
import { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, getMint } from '@solana/spl-token';
import { createRpc } from '@lightprotocol/stateless.js';
import {
getAssociatedTokenAddressInterface,
Expand Down Expand Up @@ -31,12 +31,12 @@ export function useUnifiedBalance() {
const owner = new PublicKey(ownerAddress);

// Per-mint accumulator
const mintMap = new Map<string, { spl: bigint; t22: bigint; hot: bigint; cold: bigint; decimals: number }>();
const mintMap = new Map<string, { spl: bigint; t22: bigint; hot: bigint; cold: bigint; decimals: number; tokenProgram: PublicKey }>();

const getOrCreate = (mintStr: string) => {
let entry = mintMap.get(mintStr);
if (!entry) {
entry = { spl: 0n, t22: 0n, hot: 0n, cold: 0n, decimals: 9 };
entry = { spl: 0n, t22: 0n, hot: 0n, cold: 0n, decimals: 9, tokenProgram: TOKEN_PROGRAM_ID };
mintMap.set(mintStr, entry);
}
return entry;
Expand Down Expand Up @@ -78,14 +78,41 @@ export function useUnifiedBalance() {
const mint = new PublicKey(buf.subarray(0, 32));
const amount = buf.readBigUInt64LE(64);
const mintStr = mint.toBase58();
getOrCreate(mintStr).t22 += amount;
const entry = getOrCreate(mintStr);
entry.t22 += amount;
entry.tokenProgram = TOKEN_2022_PROGRAM_ID;
}
} catch {
// No Token 2022 accounts
}

// 4. Hot balance from Light Token associated token account
// 4. Cold balance from compressed token accounts
try {
const compressed = await rpc.getCompressedTokenBalancesByOwnerV2(owner);
for (const item of compressed.value.items) {
const mintStr = item.mint.toBase58();
getOrCreate(mintStr).cold += BigInt(item.balance.toString());
}
} catch {
// No compressed accounts
}

// 5. Fetch actual decimals for each mint
const mintKeys = [...mintMap.keys()];
await Promise.allSettled(
mintKeys.map(async (mintStr) => {
try {
const mint = new PublicKey(mintStr);
const entry = getOrCreate(mintStr);
const mintInfo = await getMint(rpc, mint, undefined, entry.tokenProgram);
entry.decimals = mintInfo.decimals;
} catch {
// Keep default decimals if mint fetch fails
}
}),
);

// 6. Hot balance from Light Token associated token account
await Promise.allSettled(
mintKeys.map(async (mintStr) => {
try {
Expand All @@ -100,18 +127,7 @@ export function useUnifiedBalance() {
}),
);

// 5. Cold balance from compressed token accounts
try {
const compressed = await rpc.getCompressedTokenBalancesByOwnerV2(owner);
for (const item of compressed.value.items) {
const mintStr = item.mint.toBase58();
getOrCreate(mintStr).cold += BigInt(item.balance.toString());
}
} catch {
// No compressed accounts
}

// 6. Assemble TokenBalance[]
// 7. Assemble TokenBalance[]
const result: TokenBalance[] = [];

// SOL entry
Expand Down
2 changes: 1 addition & 1 deletion toolkits/sign-with-privy/react/src/hooks/useUnwrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function useUnwrap() {

const owner = new PublicKey(ownerPublicKey);
const mintPubkey = new PublicKey(mint);
const tokenAmount = BigInt(Math.floor(amount * Math.pow(10, decimals)));
const tokenAmount = BigInt(Math.round(amount * Math.pow(10, decimals)));

// Auto-detect token program (SPL vs T22) from mint account owner
const mintAccountInfo = await rpc.getAccountInfo(mintPubkey);
Expand Down
2 changes: 1 addition & 1 deletion toolkits/sign-with-privy/react/src/hooks/useWrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function useWrap() {

const owner = new PublicKey(ownerPublicKey);
const mintPubkey = new PublicKey(mint);
const tokenAmount = BigInt(Math.floor(amount * Math.pow(10, decimals)));
const tokenAmount = BigInt(Math.round(amount * Math.pow(10, decimals)));

// Get SPL interface info — determines whether mint uses SPL or T22
const splInterfaceInfos = await getSplInterfaceInfos(rpc, mintPubkey);
Expand Down
122 changes: 122 additions & 0 deletions toolkits/sign-with-wallet-adapter/react/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Wallet Adapter + Light Token (React)

Wallet Adapter handles wallet connection and transaction signing. You build transactions with light-token instructions and the connected wallet signs them client-side:

1. Connect wallet via Wallet Adapter
2. Build unsigned transaction
3. Sign transaction using connected wallet (Phantom, Backpack, Solflare, etc.)
4. Send signed transaction to RPC

Light Token gives you rent-free token accounts on Solana. Light-token accounts hold balances from any light, SPL, or Token-2022 mint.


## What you will implement

| | SPL | Light Token |
| --- | --- | --- |
| [**Transfer**](#hooks) | `transferChecked()` | `createTransferInterfaceInstruction()` |
| [**Wrap**](#hooks) | N/A | `createWrapInstruction()` |
| [**Get balance**](#hooks) | `getAccount()` | `getAtaInterface()` |
| [**Transaction history**](#hooks) | `getSignaturesForAddress()` | `getSignaturesForOwnerInterface()` |

### Source files

#### Hooks

- **[useTransfer.ts](src/hooks/useTransfer.ts)** — Transfer light-tokens between wallets. Auto-loads cold balance before sending.
- **[useWrap.ts](src/hooks/useWrap.ts)** — Wrap SPL or T22 tokens into light-token associated token account. Auto-detects token program.
- **[useUnwrap.ts](src/hooks/useUnwrap.ts)** — Unwrap light-token associated token account back to SPL or T22. Hook only, not wired into UI.
- **[useLightBalance.ts](src/hooks/useLightBalance.ts)** — Query hot, cold, and unified Light Token balance for a single mint.
- **[useUnifiedBalance.ts](src/hooks/useUnifiedBalance.ts)** — Query balance breakdown: SOL, SPL, Token 2022, light-token hot, and compressed cold.
- **[useTransactionHistory.ts](src/hooks/useTransactionHistory.ts)** — Fetch transaction history for light-token operations.

#### Components

- **[TransferForm.tsx](src/components/sections/TransferForm.tsx)** — Single "Send" button. Routes by token type: light-token -> light-token, or SPL/Token 2022 are wrapped then transfered in one transaction.
- **[TransactionHistory.tsx](src/components/sections/TransactionHistory.tsx)** — Recent light-token interface transactions with explorer links.
- **[WalletInfo.tsx](src/components/sections/WalletInfo.tsx)** — Wallet address display.
- **[TransactionStatus.tsx](src/components/sections/TransactionStatus.tsx)** — Last transaction signature with explorer link.

> Light Token is currently deployed on **devnet**. The interface PDA pattern described here applies to mainnet.

## Before you start

### Your mint needs an SPL interface PDA

The interface PDA enables interoperability between SPL/T22 and light-token. It holds SPL/T22 tokens when they're wrapped into light-token format.

**Check if your mint has one:**

```typescript
import { getSplInterfaceInfos } from "@lightprotocol/compressed-token";

const infos = await getSplInterfaceInfos(rpc, mint);
const hasInterface = infos.some((info) => info.isInitialized);
```

**Register one if it doesn't:**

```bash
# For an existing SPL or T22 mint (from scripts/)
cd ../scripts && npm run register:spl-interface <mint-address>
```

Or in code via `createSplInterface(rpc, payer, mint)`. Works with both SPL Token and Token-2022 mints.

**Example: wrapping devnet USDC.** If you have devnet USDC (`4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`), register its interface PDA first, then wrap it into a light-token associated token account. Set `TEST_MINT` in `.env` to the USDC mint address.

## Setup

```bash
npm install
cp .env.example .env
# Fill in your credentials
```

### Environment variables

| Variable | Description |
| -------- | ----------- |
| `VITE_HELIUS_RPC_URL` | Helius RPC endpoint (e.g. `https://devnet.helius-rpc.com?api-key=...`). Required for ZK compression indexing. |

### Setup helpers (local keypair)

Setup scripts live in [`scripts/`](../scripts/). They use the Solana CLI keypair at `~/.config/solana/id.json`.

```bash
cd ../scripts
cp .env.example .env # set HELIUS_RPC_URL
```

| Command | What it does |
| ------- | ----------- |
| `npm run mint:spl-and-wrap <recipient> [amount] [decimals]` | Create an SPL or T22 mint with interface PDA, mint tokens, wrap, and transfer to recipient. |
| `npm run mint:spl <mint> <recipient> [amount] [decimals]` | Mint additional SPL or T22 tokens to an existing mint. |
| `npm run register:spl-interface <mint>` | Register an interface PDA on an existing SPL or T22 mint. Required for wrap/unwrap. |

## Quick start

```bash
# 1. Create a test mint with interface PDA + fund your wallet
cd ../scripts && npm run mint:spl-and-wrap <your-wallet-address>

# 2. Start the dev server
cd ../react && npm run dev
```

Then in the browser:
1. Connect your wallet via the Wallet Adapter modal
2. Select a light-token balance from the dropdown
3. Enter a recipient address and amount
4. Click "Send" — the app transfers directly
5. Select an SPL balance — the app wraps to light-token then transfers (two signing prompts)

## Tests

```bash
# Unit tests (no network)
pnpm test

# Integration tests (devnet)
VITE_HELIUS_RPC_URL=https://devnet.helius-rpc.com?api-key=... pnpm test:integration
```
Loading