Skip to content

Commit a3d0aff

Browse files
klausundklausclaudetilo-14devin-ai-integration[bot]tilo-14
authored
feat: add sign-with-wallet-adapter React example (#29)
* feat: add sign-with-wallet-adapter React example Adds a Wallet Adapter equivalent of the existing Privy example. Swaps the signing layer from Privy SDK to @solana/wallet-adapter-react while keeping identical Light Token SDK usage (transfer, wrap, unwrap). Changes from Privy example: - ConnectionProvider + WalletProvider + WalletModalProvider (replaces PrivyProvider) - useWallet().signTransaction (replaces useSignTransaction from Privy) - signAndSendBatches takes signTransaction(Transaction) -> Transaction - WalletMultiButton for connect/disconnect (replaces Privy login/logout) - Removed wallet selector (single connected wallet) - Fixed hot/cold balance fetch order (cold before hot) so compressed-only mints also get hot balance lookup 26 unit tests passing, build clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add confirmTransaction to useWrap, expand README - useWrap now awaits confirmTransaction after sendRawTransaction, matching the behavior of signAndSendBatches (used by transfer/unwrap) - Added confirmTransaction assertion to wrap test - README expanded to match Privy example quality: hook docs, component docs, setup helpers table, quick start guide Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add e2e integration test for wallet-adapter hooks Tests useUnifiedBalance, useTransfer, and useTransactionHistory against devnet using filesystem keypair signing. Creates a fresh mint with SPL interface, mints tokens, wraps to light-token, then exercises the full transfer + balance + history flow. Run: VITE_HELIUS_RPC_URL=<url> pnpm test:integration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add localnet mode to e2e integration test Support both devnet and localnet testing: - VITE_HELIUS_RPC_URL=<url> for devnet (Helius bundles compression API) - VITE_LOCALNET=true for localnet (createRpc() uses default ports 8899/8784/3001) - Localnet mode airdrops SOL before running tests Note: localnet currently blocked by CLI/SDK version mismatch (CLI v0.27.0 programs don't match SDK v0.23.0-beta.9 instructions). Devnet tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Apply suggestions from code review Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: resolve Devin review — fetch mint decimals, consistent Math.round - useUnifiedBalance: fetch actual mint decimals via getMint() instead of hardcoding 9 (fixes wrong display/amounts for USDC and other non-9 decimal tokens) - useWrap, useUnwrap: align on Math.round to match useTransfer (all 3 hooks now consistent) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: apply same decimals + Math.round fixes to Privy example Mirror of the wallet-adapter fixes: - useUnifiedBalance: fetch actual mint decimals via getMint() - useUnifiedBalance: fix hot/cold fetch order (cold before hot) so mints discovered only via compressed accounts also get hot balance lookup - useTransfer, useWrap, useUnwrap: Math.floor → Math.round Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: pass correct programId to getMint for Token 2022 mints getMint() defaults to TOKEN_PROGRAM_ID, silently failing for T22 mints and falling back to hardcoded decimals: 9. Track tokenProgram per mint in mintMap and pass it through to getMint(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: refactor Node.js balance snippet to store raw bigints Same four bugs as React versions: - toUiAmount(amount, 9) called early, hardcoding 9 decimals - Hot balance fetched before cold (ordering bug) - decimals: 9 never updated from on-chain - No tokenProgram tracking for T22 getMint Refactored to store raw bigint values in accumulator and convert to UI amounts at assembly time using actual mint decimals. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add wallet-adapter and sponsor-rent-top-ups to READMEs List all 5 toolkits in both root and toolkits README. Add missing pinocchio-swap to program examples table. Entire-Checkpoint: 4afa49be1970 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: tilo-14 <tilo@luminouslabs.org> Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: tilo-14 <tilo@luminouslabs.com>
1 parent bc5b584 commit a3d0aff

42 files changed

Lines changed: 2494 additions & 49 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

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

1819
## Client Examples

toolkits/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ Light-token operations signed with [Privy](https://privy.io) wallets. Server-sid
1919
- **[React](sign-with-privy/react/)** — Browser app using `@privy-io/react-auth` with embedded wallet signing
2020
- **[Setup scripts](sign-with-privy/scripts/)** — Create test mints and fund wallets on devnet
2121

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

2429
[Rust program example to stream mint events](streaming-tokens/) of the Light-Token Program.

toolkits/sign-with-privy/nodejs/src/balances.ts

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'dotenv/config';
22
import {PublicKey, LAMPORTS_PER_SOL} from '@solana/web3.js';
3-
import {TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID} from '@solana/spl-token';
3+
import {TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, getMint} from '@solana/spl-token';
44
import {createRpc} from '@lightprotocol/stateless.js';
55
import {
66
getAtaInterface,
@@ -36,13 +36,13 @@ export async function getBalances(
3636
console.error('Failed to fetch SOL balance:', e);
3737
}
3838

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

4242
const getOrCreate = (mintStr: string) => {
4343
let entry = mintMap.get(mintStr);
4444
if (!entry) {
45-
entry = {spl: 0, t22: 0, hot: 0, cold: 0, decimals: 9};
45+
entry = {spl: 0n, t22: 0n, hot: 0n, cold: 0n, decimals: 9, tokenProgram: TOKEN_PROGRAM_ID};
4646
mintMap.set(mintStr, entry);
4747
}
4848
return entry;
@@ -59,7 +59,7 @@ export async function getBalances(
5959
const mint = new PublicKey(buf.subarray(0, 32));
6060
const amount = buf.readBigUInt64LE(64);
6161
const mintStr = mint.toBase58();
62-
getOrCreate(mintStr).spl += toUiAmount(amount, 9);
62+
getOrCreate(mintStr).spl += amount;
6363
}
6464
} catch {
6565
// No SPL accounts
@@ -76,49 +76,66 @@ export async function getBalances(
7676
const mint = new PublicKey(buf.subarray(0, 32));
7777
const amount = buf.readBigUInt64LE(64);
7878
const mintStr = mint.toBase58();
79-
getOrCreate(mintStr).t22 += toUiAmount(amount, 9);
79+
const entry = getOrCreate(mintStr);
80+
entry.t22 += amount;
81+
entry.tokenProgram = TOKEN_2022_PROGRAM_ID;
8082
}
8183
} catch {
8284
// No Token 2022 accounts
8385
}
8486

85-
// 3. Hot balance from Light Token associated token account
87+
// 3. Cold balance from compressed token accounts
88+
try {
89+
const compressed = await rpc.getCompressedTokenBalancesByOwnerV2(owner);
90+
for (const item of compressed.value.items) {
91+
const mintStr = item.mint.toBase58();
92+
getOrCreate(mintStr).cold += BigInt(item.balance.toString());
93+
}
94+
} catch {
95+
// No compressed accounts
96+
}
97+
98+
// 4. Fetch actual decimals for each mint
8699
const mintKeys = [...mintMap.keys()];
100+
await Promise.allSettled(
101+
mintKeys.map(async (mintStr) => {
102+
try {
103+
const mint = new PublicKey(mintStr);
104+
const entry = getOrCreate(mintStr);
105+
const mintInfo = await getMint(rpc, mint, undefined, entry.tokenProgram);
106+
entry.decimals = mintInfo.decimals;
107+
} catch {
108+
// Keep default decimals if mint fetch fails
109+
}
110+
}),
111+
);
112+
113+
// 5. Hot balance from Light Token associated token account
87114
await Promise.allSettled(
88115
mintKeys.map(async (mintStr) => {
89116
try {
90117
const mint = new PublicKey(mintStr);
91118
const ata = getAssociatedTokenAddressInterface(mint, owner);
92119
const {parsed} = await getAtaInterface(rpc, ata, owner, mint);
93-
getOrCreate(mintStr).hot = toUiAmount(parsed.amount, 9);
120+
getOrCreate(mintStr).hot = BigInt(parsed.amount.toString());
94121
} catch {
95122
// Associated token account does not exist for this mint
96123
}
97124
}),
98125
);
99126

100-
// 4. Cold balance from compressed token accounts
101-
try {
102-
const compressed = await rpc.getCompressedTokenBalancesByOwnerV2(owner);
103-
for (const item of compressed.value.items) {
104-
const mintStr = item.mint.toBase58();
105-
getOrCreate(mintStr).cold += toUiAmount(BigInt(item.balance.toString()), 9);
106-
}
107-
} catch {
108-
// No compressed accounts
109-
}
110-
111-
// Assemble result
127+
// 6. Assemble result (convert raw → UI amounts here)
112128
const tokens: TokenBalance[] = [];
113129
for (const [mintStr, entry] of mintMap) {
130+
const d = entry.decimals;
114131
tokens.push({
115132
mint: mintStr,
116-
decimals: entry.decimals,
117-
hot: entry.hot,
118-
cold: entry.cold,
119-
spl: entry.spl,
120-
t22: entry.t22,
121-
unified: entry.hot + entry.cold,
133+
decimals: d,
134+
hot: toUiAmount(entry.hot, d),
135+
cold: toUiAmount(entry.cold, d),
136+
spl: toUiAmount(entry.spl, d),
137+
t22: toUiAmount(entry.t22, d),
138+
unified: toUiAmount(entry.hot + entry.cold, d),
122139
});
123140
}
124141

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

134-
function toUiAmount(raw: bigint | {toNumber: () => number}, decimals: number): number {
135-
const value = typeof raw === 'bigint' ? Number(raw) : raw.toNumber();
136-
return value / 10 ** decimals;
151+
function toUiAmount(raw: bigint, decimals: number): number {
152+
return Number(raw) / 10 ** decimals;
137153
}
138154

139155
export default getBalances;

toolkits/sign-with-privy/react/src/hooks/useTransfer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function useTransfer() {
3939
const owner = new PublicKey(ownerPublicKey);
4040
const mintPubkey = new PublicKey(mint);
4141
const recipient = new PublicKey(toAddress);
42-
const tokenAmount = Math.floor(amount * Math.pow(10, decimals));
42+
const tokenAmount = Math.round(amount * Math.pow(10, decimals));
4343

4444
// Returns TransactionInstruction[][].
4545
// Each inner array is one transaction.

toolkits/sign-with-privy/react/src/hooks/useUnifiedBalance.ts

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState, useCallback } from 'react';
22
import { PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';
3-
import { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from '@solana/spl-token';
3+
import { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, getMint } from '@solana/spl-token';
44
import { createRpc } from '@lightprotocol/stateless.js';
55
import {
66
getAssociatedTokenAddressInterface,
@@ -31,12 +31,12 @@ export function useUnifiedBalance() {
3131
const owner = new PublicKey(ownerAddress);
3232

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

3636
const getOrCreate = (mintStr: string) => {
3737
let entry = mintMap.get(mintStr);
3838
if (!entry) {
39-
entry = { spl: 0n, t22: 0n, hot: 0n, cold: 0n, decimals: 9 };
39+
entry = { spl: 0n, t22: 0n, hot: 0n, cold: 0n, decimals: 9, tokenProgram: TOKEN_PROGRAM_ID };
4040
mintMap.set(mintStr, entry);
4141
}
4242
return entry;
@@ -78,14 +78,41 @@ export function useUnifiedBalance() {
7878
const mint = new PublicKey(buf.subarray(0, 32));
7979
const amount = buf.readBigUInt64LE(64);
8080
const mintStr = mint.toBase58();
81-
getOrCreate(mintStr).t22 += amount;
81+
const entry = getOrCreate(mintStr);
82+
entry.t22 += amount;
83+
entry.tokenProgram = TOKEN_2022_PROGRAM_ID;
8284
}
8385
} catch {
8486
// No Token 2022 accounts
8587
}
8688

87-
// 4. Hot balance from Light Token associated token account
89+
// 4. Cold balance from compressed token accounts
90+
try {
91+
const compressed = await rpc.getCompressedTokenBalancesByOwnerV2(owner);
92+
for (const item of compressed.value.items) {
93+
const mintStr = item.mint.toBase58();
94+
getOrCreate(mintStr).cold += BigInt(item.balance.toString());
95+
}
96+
} catch {
97+
// No compressed accounts
98+
}
99+
100+
// 5. Fetch actual decimals for each mint
88101
const mintKeys = [...mintMap.keys()];
102+
await Promise.allSettled(
103+
mintKeys.map(async (mintStr) => {
104+
try {
105+
const mint = new PublicKey(mintStr);
106+
const entry = getOrCreate(mintStr);
107+
const mintInfo = await getMint(rpc, mint, undefined, entry.tokenProgram);
108+
entry.decimals = mintInfo.decimals;
109+
} catch {
110+
// Keep default decimals if mint fetch fails
111+
}
112+
}),
113+
);
114+
115+
// 6. Hot balance from Light Token associated token account
89116
await Promise.allSettled(
90117
mintKeys.map(async (mintStr) => {
91118
try {
@@ -100,18 +127,7 @@ export function useUnifiedBalance() {
100127
}),
101128
);
102129

103-
// 5. Cold balance from compressed token accounts
104-
try {
105-
const compressed = await rpc.getCompressedTokenBalancesByOwnerV2(owner);
106-
for (const item of compressed.value.items) {
107-
const mintStr = item.mint.toBase58();
108-
getOrCreate(mintStr).cold += BigInt(item.balance.toString());
109-
}
110-
} catch {
111-
// No compressed accounts
112-
}
113-
114-
// 6. Assemble TokenBalance[]
130+
// 7. Assemble TokenBalance[]
115131
const result: TokenBalance[] = [];
116132

117133
// SOL entry

toolkits/sign-with-privy/react/src/hooks/useUnwrap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function useUnwrap() {
3838

3939
const owner = new PublicKey(ownerPublicKey);
4040
const mintPubkey = new PublicKey(mint);
41-
const tokenAmount = BigInt(Math.floor(amount * Math.pow(10, decimals)));
41+
const tokenAmount = BigInt(Math.round(amount * Math.pow(10, decimals)));
4242

4343
// Auto-detect token program (SPL vs T22) from mint account owner
4444
const mintAccountInfo = await rpc.getAccountInfo(mintPubkey);

toolkits/sign-with-privy/react/src/hooks/useWrap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function useWrap() {
4040

4141
const owner = new PublicKey(ownerPublicKey);
4242
const mintPubkey = new PublicKey(mint);
43-
const tokenAmount = BigInt(Math.floor(amount * Math.pow(10, decimals)));
43+
const tokenAmount = BigInt(Math.round(amount * Math.pow(10, decimals)));
4444

4545
// Get SPL interface info — determines whether mint uses SPL or T22
4646
const splInterfaceInfos = await getSplInterfaceInfos(rpc, mintPubkey);
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Wallet Adapter + Light Token (React)
2+
3+
Wallet Adapter handles wallet connection and transaction signing. You build transactions with light-token instructions and the connected wallet signs them client-side:
4+
5+
1. Connect wallet via Wallet Adapter
6+
2. Build unsigned transaction
7+
3. Sign transaction using connected wallet (Phantom, Backpack, Solflare, etc.)
8+
4. Send signed transaction to RPC
9+
10+
Light Token gives you rent-free token accounts on Solana. Light-token accounts hold balances from any light, SPL, or Token-2022 mint.
11+
12+
13+
## What you will implement
14+
15+
| | SPL | Light Token |
16+
| --- | --- | --- |
17+
| [**Transfer**](#hooks) | `transferChecked()` | `createTransferInterfaceInstruction()` |
18+
| [**Wrap**](#hooks) | N/A | `createWrapInstruction()` |
19+
| [**Get balance**](#hooks) | `getAccount()` | `getAtaInterface()` |
20+
| [**Transaction history**](#hooks) | `getSignaturesForAddress()` | `getSignaturesForOwnerInterface()` |
21+
22+
### Source files
23+
24+
#### Hooks
25+
26+
- **[useTransfer.ts](src/hooks/useTransfer.ts)** — Transfer light-tokens between wallets. Auto-loads cold balance before sending.
27+
- **[useWrap.ts](src/hooks/useWrap.ts)** — Wrap SPL or T22 tokens into light-token associated token account. Auto-detects token program.
28+
- **[useUnwrap.ts](src/hooks/useUnwrap.ts)** — Unwrap light-token associated token account back to SPL or T22. Hook only, not wired into UI.
29+
- **[useLightBalance.ts](src/hooks/useLightBalance.ts)** — Query hot, cold, and unified Light Token balance for a single mint.
30+
- **[useUnifiedBalance.ts](src/hooks/useUnifiedBalance.ts)** — Query balance breakdown: SOL, SPL, Token 2022, light-token hot, and compressed cold.
31+
- **[useTransactionHistory.ts](src/hooks/useTransactionHistory.ts)** — Fetch transaction history for light-token operations.
32+
33+
#### Components
34+
35+
- **[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.
36+
- **[TransactionHistory.tsx](src/components/sections/TransactionHistory.tsx)** — Recent light-token interface transactions with explorer links.
37+
- **[WalletInfo.tsx](src/components/sections/WalletInfo.tsx)** — Wallet address display.
38+
- **[TransactionStatus.tsx](src/components/sections/TransactionStatus.tsx)** — Last transaction signature with explorer link.
39+
40+
> Light Token is currently deployed on **devnet**. The interface PDA pattern described here applies to mainnet.
41+
42+
## Before you start
43+
44+
### Your mint needs an SPL interface PDA
45+
46+
The interface PDA enables interoperability between SPL/T22 and light-token. It holds SPL/T22 tokens when they're wrapped into light-token format.
47+
48+
**Check if your mint has one:**
49+
50+
```typescript
51+
import { getSplInterfaceInfos } from "@lightprotocol/compressed-token";
52+
53+
const infos = await getSplInterfaceInfos(rpc, mint);
54+
const hasInterface = infos.some((info) => info.isInitialized);
55+
```
56+
57+
**Register one if it doesn't:**
58+
59+
```bash
60+
# For an existing SPL or T22 mint (from scripts/)
61+
cd ../scripts && npm run register:spl-interface <mint-address>
62+
```
63+
64+
Or in code via `createSplInterface(rpc, payer, mint)`. Works with both SPL Token and Token-2022 mints.
65+
66+
**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.
67+
68+
## Setup
69+
70+
```bash
71+
npm install
72+
cp .env.example .env
73+
# Fill in your credentials
74+
```
75+
76+
### Environment variables
77+
78+
| Variable | Description |
79+
| -------- | ----------- |
80+
| `VITE_HELIUS_RPC_URL` | Helius RPC endpoint (e.g. `https://devnet.helius-rpc.com?api-key=...`). Required for ZK compression indexing. |
81+
82+
### Setup helpers (local keypair)
83+
84+
Setup scripts live in [`scripts/`](../scripts/). They use the Solana CLI keypair at `~/.config/solana/id.json`.
85+
86+
```bash
87+
cd ../scripts
88+
cp .env.example .env # set HELIUS_RPC_URL
89+
```
90+
91+
| Command | What it does |
92+
| ------- | ----------- |
93+
| `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. |
94+
| `npm run mint:spl <mint> <recipient> [amount] [decimals]` | Mint additional SPL or T22 tokens to an existing mint. |
95+
| `npm run register:spl-interface <mint>` | Register an interface PDA on an existing SPL or T22 mint. Required for wrap/unwrap. |
96+
97+
## Quick start
98+
99+
```bash
100+
# 1. Create a test mint with interface PDA + fund your wallet
101+
cd ../scripts && npm run mint:spl-and-wrap <your-wallet-address>
102+
103+
# 2. Start the dev server
104+
cd ../react && npm run dev
105+
```
106+
107+
Then in the browser:
108+
1. Connect your wallet via the Wallet Adapter modal
109+
2. Select a light-token balance from the dropdown
110+
3. Enter a recipient address and amount
111+
4. Click "Send" — the app transfers directly
112+
5. Select an SPL balance — the app wraps to light-token then transfers (two signing prompts)
113+
114+
## Tests
115+
116+
```bash
117+
# Unit tests (no network)
118+
pnpm test
119+
120+
# Integration tests (devnet)
121+
VITE_HELIUS_RPC_URL=https://devnet.helius-rpc.com?api-key=... pnpm test:integration
122+
```

0 commit comments

Comments
 (0)