Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
30 changes: 30 additions & 0 deletions toolkits/sign-with-wallet-adapter/react/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# React + Wallet Adapter — Light Token Example

Send, wrap, and unwrap Light Tokens using `@solana/wallet-adapter-react`.

## Setup

```bash
cp .env.example .env
# Fill in VITE_HELIUS_RPC_URL (devnet)

pnpm install
pnpm dev
```

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

## Stack

- React 19, Vite, Tailwind v4
- `@solana/wallet-adapter-react` + `@solana/wallet-adapter-react-ui`
- `@lightprotocol/compressed-token` (unified interface)
- `@solana/web3.js` 1.x
13 changes: 13 additions & 0 deletions toolkits/sign-with-wallet-adapter/react/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Wallet Adapter Light Token</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
38 changes: 38 additions & 0 deletions toolkits/sign-with-wallet-adapter/react/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "react-wallet-adapter-light-token",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest",
"test:integration": "vitest run --config vitest.integration.config.ts"
},
"dependencies": {
"@heroicons/react": "^2.2.0",
"@lightprotocol/compressed-token": "beta",
"@lightprotocol/stateless.js": "beta",
"@solana/spl-token": "^0.4.13",
"@solana/wallet-adapter-react": "^0.15.35",
"@solana/wallet-adapter-react-ui": "^0.9.35",
"@solana/web3.js": "1.98.4",
"buffer": "^6.0.3",
"react": "^19.1.1",
"react-dom": "^19.1.1"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.18",
"@testing-library/react": "^16.3.2",
"@types/react": "^19.1.1",
"@types/react-dom": "^19.1.1",
"@vitejs/plugin-react": "^4.3.4",
"happy-dom": "^20.6.1",
"tailwindcss": "^4.1.18",
"typescript": "^5.8.3",
"vite": "^6.0.11",
"vite-plugin-node-polyfills": "^0.25.0",
"vitest": "^4.0.18"
}
}
95 changes: 95 additions & 0 deletions toolkits/sign-with-wallet-adapter/react/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useState, useEffect } from 'react';
import { useWallet } from '@solana/wallet-adapter-react';
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
import { useUnifiedBalance } from './hooks/useUnifiedBalance';
import { Header } from './components/ui/Header';
import WalletInfo from './components/sections/WalletInfo';
import TransferForm from './components/sections/TransferForm';
import TransactionStatus from './components/sections/TransactionStatus';
import TransactionHistory from './components/sections/TransactionHistory';

export default function App() {
const { publicKey, connected } = useWallet();
const { balances, isLoading: isLoadingBalances, fetchBalances } = useUnifiedBalance();

const [selectedMint, setSelectedMint] = useState<string>('');
const [txSignature, setTxSignature] = useState<string | null>(null);
const [txError, setTxError] = useState<string | null>(null);

const ownerAddress = publicKey?.toBase58() ?? '';

useEffect(() => {
if (!ownerAddress) {
setSelectedMint('');
return;
}

const loadBalances = async () => {
await fetchBalances(ownerAddress);
};

loadBalances();
}, [ownerAddress, fetchBalances]);

useEffect(() => {
if (balances.length > 0 && !selectedMint) {
setSelectedMint(balances[0].mint);
}
}, [balances, selectedMint]);

const handleTransferSuccess = async (signature: string) => {
setTxSignature(signature);
setTxError(null);
await fetchBalances(ownerAddress);
};

const handleTransferError = (error: string) => {
setTxError(error);
setTxSignature(null);
};

return (
<div className="bg-[#E0E7FF66] min-h-screen">
<Header />
{connected && publicKey ? (
<section className="w-full p-8">
<div className="flex items-center justify-between mb-8">
<WalletMultiButton />
</div>

<div className="flex justify-center">
<div className="w-full max-w-2xl">
<WalletInfo address={ownerAddress} />

<TransferForm
ownerAddress={ownerAddress}
selectedMint={selectedMint}
onMintChange={setSelectedMint}
balances={balances}
isLoadingBalances={isLoadingBalances}
onTransferSuccess={handleTransferSuccess}
onTransferError={handleTransferError}
/>

<TransactionStatus signature={txSignature} error={txError} />

<TransactionHistory ownerAddress={ownerAddress} refreshTrigger={txSignature} />
</div>
</div>
</section>
) : (
<section className="w-full flex flex-col justify-center items-center h-[calc(100vh-60px)] px-4">
<div className="text-center max-w-md">
<h1 className="text-3xl md:text-4xl font-semibold text-gray-900 mb-3">
Send Tokens
</h1>
<p className="text-gray-600 mb-8">
Send light tokens to any Solana address instantly.
</p>
<WalletMultiButton />
</div>
</section>
)}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useState } from 'react';
import { ClipboardIcon, CheckIcon } from '@heroicons/react/24/outline';

interface CopyButtonProps {
text: string;
label?: string;
}

export default function CopyButton({ text, label }: CopyButtonProps) {
const [copied, setCopied] = useState(false);

const handleCopy = async () => {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};

return (
<button
onClick={handleCopy}
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
title={`Copy ${label || 'text'}`}
>
{copied ? (
<CheckIcon className="h-4 w-4 text-green-500" />
) : (
<ClipboardIcon className="h-4 w-4" />
)}
</button>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ReactNode } from 'react';

interface SectionProps {
name: string;
children: ReactNode;
}

export default function Section({ name, children }: SectionProps) {
return (
<div className="section">
<h2 className="text-lg font-semibold text-gray-900 mb-4">{name}</h2>
{children}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useEffect, useState } from 'react';
import { useTransactionHistory } from '../../hooks/useTransactionHistory';
import Section from '../reusables/Section';
import CopyButton from '../reusables/CopyButton';

interface TransactionHistoryProps {
ownerAddress: string;
refreshTrigger?: string | null;
}

export default function TransactionHistory({ ownerAddress, refreshTrigger }: TransactionHistoryProps) {
const { transactions, isLoading, error, fetchTransactionHistory } = useTransactionHistory();
const [isExpanded, setIsExpanded] = useState(false);

useEffect(() => {
if (ownerAddress) {
fetchTransactionHistory(ownerAddress);
}
}, [ownerAddress, refreshTrigger, fetchTransactionHistory]);

if (!ownerAddress) return null;

return (
<Section name="Transaction History">
{isLoading && <p className="text-sm text-gray-500">Loading...</p>}
{error && <p className="text-sm text-red-600">{error}</p>}
{!isLoading && transactions.length === 0 && !error && (
<p className="text-sm text-gray-500">No transactions found.</p>
)}
{transactions.length > 0 && (
<div className="space-y-2">
{transactions.slice(0, isExpanded ? undefined : 5).map((tx) => (
<div
key={tx.signature}
className="flex items-center justify-between py-2 border-b border-gray-100 last:border-0"
>
<div>
<p className="font-mono text-xs text-gray-700">
{tx.signature.slice(0, 16)}...{tx.signature.slice(-8)}
</p>
{tx.timestamp && (
<p className="text-xs text-gray-400">
{new Date(tx.timestamp).toLocaleString()}
</p>
)}
</div>
<div className="flex items-center gap-2">
<CopyButton text={tx.signature} label="Signature" />
<a
href={`https://explorer.solana.com/tx/${tx.signature}?cluster=devnet`}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-indigo-600 hover:text-indigo-800"
>
Explorer
</a>
</div>
</div>
))}
{transactions.length > 5 && (
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-xs text-indigo-600 hover:text-indigo-800"
>
{isExpanded ? 'Show less' : `Show all (${transactions.length})`}
</button>
)}
</div>
)}
</Section>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Section from '../reusables/Section';
import CopyButton from '../reusables/CopyButton';

interface TransactionStatusProps {
signature: string | null;
error: string | null;
}

export default function TransactionStatus({ signature, error }: TransactionStatusProps) {
if (!signature && !error) return null;

return (
<Section name="Transaction Result">
{signature && (
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-green-800">Success!</p>
<p className="text-xs text-green-600 mt-1 font-mono">
{signature.slice(0, 20)}...{signature.slice(-20)}
</p>
</div>
<div className="flex gap-2">
<CopyButton text={signature} label="Signature" />
<a
href={`https://explorer.solana.com/tx/${signature}?cluster=devnet`}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-green-700 hover:text-green-900 underline"
>
View on Explorer
</a>
</div>
</div>
</div>
)}
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm font-medium text-red-800">Error</p>
<p className="text-xs text-red-600 mt-1">{error}</p>
</div>
)}
</Section>
);
}
Loading