Skip to content

Commit 33ecef0

Browse files
committed
git commit -m "refactor(nextjs-anchor): migrate to Kit plugins for wallet and transaction handling"
1 parent bb687a7 commit 33ecef0

27 files changed

+4589
-2884
lines changed

kit/nextjs-anchor/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,6 @@ next-env.d.ts
4343
# anchor build artifacts
4444
/anchor/.anchor/
4545
/anchor/target/
46+
47+
# solana local validator
48+
test-ledger/

kit/nextjs-anchor/README.md

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# nextjs-anchor
22

3-
Next.js starter with Tailwind CSS, `@solana/react-hooks`, and an Anchor vault program example.
3+
Next.js starter with Tailwind CSS, `@solana/kit`, and an Anchor vault program example.
44

55
## Getting Started
66

@@ -9,43 +9,90 @@ npx -y create-solana-dapp@latest -t solana-foundation/templates/kit/nextjs-ancho
99
```
1010

1111
```shell
12-
npm install # Builds program and generates client automatically
12+
npm install
13+
npm run setup # Builds the Anchor program and generates the TypeScript client
1314
npm run dev
1415
```
1516

16-
Open [http://localhost:3000](http://localhost:3000), connect your wallet, and interact with the vault on devnet.
17+
Open [http://localhost:3000](http://localhost:3000), connect your wallet, and interact with the vault.
1718

1819
## What's Included
1920

20-
- **Wallet connection** via `@solana/react-hooks` with auto-discovery
21-
- **SOL Vault program** - deposit and withdraw SOL from a personal PDA vault
22-
- **Codama-generated client** - type-safe program interactions using `@solana/kit`
23-
- **Tailwind CSS v4** with light/dark mode
21+
- **Wallet connection** via wallet-standard with auto-discovery and dropdown UI
22+
- **Cluster switching** — devnet, testnet, mainnet, and localnet from the header
23+
- **Wallet balance** display with airdrop button (devnet/testnet/localnet)
24+
- **SOL Vault program** — deposit and withdraw SOL from a personal PDA vault
25+
- **Toast notifications** with explorer links for every transaction
26+
- **Error handling** — human-readable messages for common Solana and program errors
27+
- **Codama-generated client** — type-safe program interactions using `@solana/kit`
28+
- **Tailwind CSS v4** with light/dark mode toggle
2429

2530
## Stack
2631

27-
| Layer | Technology |
28-
| -------------- | --------------------------------------- |
29-
| Frontend | Next.js 16, React 19, TypeScript |
30-
| Styling | Tailwind CSS v4 |
31-
| Solana Client | `@solana/client`, `@solana/react-hooks` |
32-
| Program Client | Codama-generated, `@solana/kit` |
33-
| Program | Anchor (Rust) |
32+
| Layer | Technology |
33+
| -------------- | -------------------------------- |
34+
| Frontend | Next.js 16, React 19, TypeScript |
35+
| Styling | Tailwind CSS v4 |
36+
| Solana Client | `@solana/kit`, wallet-standard |
37+
| Program Client | Codama-generated, `@solana/kit` |
38+
| Program | Anchor (Rust) |
3439

3540
## Project Structure
3641

3742
```
3843
├── app/
3944
│ ├── components/
40-
│ │ ├── providers.tsx # Solana client setup
41-
│ │ └── vault-card.tsx # Vault deposit/withdraw UI
42-
│ ├── generated/vault/ # Codama-generated program client
43-
│ └── page.tsx # Main page
44-
├── anchor/ # Anchor workspace
45-
│ └── programs/vault/ # Vault program (Rust)
46-
└── codama.json # Codama client generation config
45+
│ │ ├── cluster-context.tsx # Cluster state (React context + localStorage)
46+
│ │ ├── cluster-select.tsx # Cluster switcher dropdown
47+
│ │ ├── grid-background.tsx # Solana-branded decorative grid
48+
│ │ ├── providers.tsx # Wallet + theme providers
49+
│ │ ├── theme-toggle.tsx # Light/dark mode toggle
50+
│ │ ├── vault-card.tsx # Vault deposit/withdraw UI
51+
│ │ └── wallet-button.tsx # Wallet connect/disconnect dropdown
52+
│ ├── generated/vault/ # Codama-generated program client
53+
│ ├── lib/
54+
│ │ ├── wallet/ # Wallet-standard connection layer
55+
│ │ │ ├── types.ts # Wallet types
56+
│ │ │ ├── standard.ts # Wallet discovery + session creation
57+
│ │ │ ├── signer.ts # WalletSession → TransactionSigner
58+
│ │ │ └── context.tsx # WalletProvider + useWallet() hook
59+
│ │ ├── hooks/
60+
│ │ │ ├── use-balance.ts # SWR-based balance fetching
61+
│ │ │ └── use-send-transaction.ts # Transaction send with loading state
62+
│ │ ├── cluster.ts # Cluster endpoints + RPC factory
63+
│ │ ├── lamports.ts # SOL/lamports conversion
64+
│ │ ├── send-transaction.ts # Transaction build + sign + send pipeline
65+
│ │ ├── errors.ts # Transaction error parsing
66+
│ │ └── explorer.ts # Explorer URL builder + address helpers
67+
│ └── page.tsx # Main page
68+
├── anchor/ # Anchor workspace
69+
│ └── programs/vault/ # Vault program (Rust)
70+
└── codama.json # Codama client generation config
4771
```
4872

73+
## Local Development
74+
75+
To test against a local validator instead of devnet:
76+
77+
1. **Start a local validator**
78+
79+
```bash
80+
solana-test-validator
81+
```
82+
83+
2. **Deploy the program locally**
84+
85+
```bash
86+
solana config set --url localhost
87+
cd anchor
88+
anchor build
89+
anchor deploy
90+
cd ..
91+
npm run codama:js # Regenerate client with local program ID
92+
```
93+
94+
3. **Switch to localnet** in the app using the cluster selector in the header.
95+
4996
## Deploy Your Own Vault
5097

5198
The included vault program is already deployed to devnet. To deploy your own:
@@ -111,8 +158,8 @@ This uses [Codama](https://github.com/codama-idl/codama) to generate a type-safe
111158

112159
## Learn More
113160

114-
- [Solana Docs](https://solana.com/docs) - core concepts and guides
115-
- [Anchor Docs](https://www.anchor-lang.com/docs) - program development framework
116-
- [Deploying Programs](https://solana.com/docs/programs/deploying) - deployment guide
117-
- [framework-kit](https://github.com/solana-foundation/framework-kit) - the React hooks used here
118-
- [Codama](https://github.com/codama-idl/codama) - client generation from IDL
161+
- [Solana Docs](https://solana.com/docs) core concepts and guides
162+
- [Anchor Docs](https://www.anchor-lang.com/docs/introduction) program development framework
163+
- [Deploying Programs](https://solana.com/docs/programs/deploying) deployment guide
164+
- [@solana/kit](https://github.com/anza-xyz/kit) — Solana JavaScript SDK
165+
- [Codama](https://github.com/codama-idl/codama) client generation from IDL
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"use client";
2+
3+
import {
4+
createContext,
5+
useContext,
6+
useState,
7+
useCallback,
8+
type ReactNode,
9+
} from "react";
10+
import type { ClusterMoniker } from "../lib/solana-client";
11+
import { CLUSTERS } from "../lib/solana-client";
12+
import { getExplorerUrl } from "../lib/explorer";
13+
14+
type ClusterContextValue = {
15+
cluster: ClusterMoniker;
16+
setCluster: (cluster: ClusterMoniker) => void;
17+
getExplorerUrl: (path: string) => string;
18+
};
19+
20+
const ClusterContext = createContext<ClusterContextValue | null>(null);
21+
22+
const STORAGE_KEY = "solana-cluster";
23+
function getInitialCluster(): ClusterMoniker {
24+
if (typeof window === "undefined") return "devnet";
25+
const stored = localStorage.getItem(STORAGE_KEY);
26+
if (stored && CLUSTERS.includes(stored as ClusterMoniker)) {
27+
return stored as ClusterMoniker;
28+
}
29+
return "devnet";
30+
}
31+
32+
export { CLUSTERS };
33+
34+
export function ClusterProvider({ children }: { children: ReactNode }) {
35+
const [cluster, setClusterState] =
36+
useState<ClusterMoniker>(getInitialCluster);
37+
38+
const setCluster = useCallback((c: ClusterMoniker) => {
39+
setClusterState(c);
40+
localStorage.setItem(STORAGE_KEY, c);
41+
}, []);
42+
43+
const explorerUrl = useCallback(
44+
(path: string) => getExplorerUrl(path, cluster),
45+
[cluster]
46+
);
47+
48+
return (
49+
<ClusterContext.Provider
50+
value={{ cluster, setCluster, getExplorerUrl: explorerUrl }}
51+
>
52+
{children}
53+
</ClusterContext.Provider>
54+
);
55+
}
56+
57+
export function useCluster() {
58+
const ctx = useContext(ClusterContext);
59+
if (!ctx) throw new Error("useCluster must be used within ClusterProvider");
60+
return ctx;
61+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"use client";
2+
3+
import { useState, useRef, useEffect } from "react";
4+
import { useCluster, CLUSTERS } from "./cluster-context";
5+
6+
export function ClusterSelect() {
7+
const { cluster, setCluster } = useCluster();
8+
const [isOpen, setIsOpen] = useState(false);
9+
const ref = useRef<HTMLDivElement>(null);
10+
11+
useEffect(() => {
12+
function handleClickOutside(e: MouseEvent) {
13+
if (ref.current && !ref.current.contains(e.target as Node)) {
14+
setIsOpen(false);
15+
}
16+
}
17+
document.addEventListener("mousedown", handleClickOutside);
18+
return () => document.removeEventListener("mousedown", handleClickOutside);
19+
}, []);
20+
21+
return (
22+
<div className="relative" ref={ref}>
23+
<button
24+
onClick={() => setIsOpen(!isOpen)}
25+
className="flex cursor-pointer items-center gap-2 rounded-lg border border-border-low bg-card px-3 py-2 text-xs font-medium transition hover:bg-cream"
26+
>
27+
<span
28+
className="h-2 w-2 rounded-full"
29+
style={{
30+
backgroundColor:
31+
cluster === "mainnet"
32+
? "#22c55e"
33+
: cluster === "devnet"
34+
? "#3b82f6"
35+
: cluster === "testnet"
36+
? "#eab308"
37+
: "#a3a3a3",
38+
}}
39+
/>
40+
{cluster}
41+
</button>
42+
43+
{isOpen && (
44+
<div className="absolute right-0 top-full z-50 mt-2 w-40 rounded-xl border border-border-low bg-card p-2 shadow-lg">
45+
<div className="space-y-1">
46+
{CLUSTERS.map((c) => (
47+
<button
48+
key={c}
49+
onClick={() => {
50+
setCluster(c);
51+
setIsOpen(false);
52+
}}
53+
className={`flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-left text-xs font-medium transition hover:bg-cream ${
54+
c === cluster ? "bg-cream" : ""
55+
}`}
56+
>
57+
<span
58+
className="h-2 w-2 rounded-full"
59+
style={{
60+
backgroundColor:
61+
c === "mainnet"
62+
? "#22c55e"
63+
: c === "devnet"
64+
? "#3b82f6"
65+
: c === "testnet"
66+
? "#eab308"
67+
: "#a3a3a3",
68+
}}
69+
/>
70+
{c}
71+
</button>
72+
))}
73+
</div>
74+
</div>
75+
)}
76+
</div>
77+
);
78+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"use client";
2+
3+
export function GridBackground() {
4+
return (
5+
<div className="pointer-events-none fixed inset-0 z-0" aria-hidden="true">
6+
{/* Dark mode ambient glow */}
7+
<div
8+
className="absolute inset-0 opacity-0 dark:opacity-100 transition-opacity duration-500"
9+
style={{
10+
background: [
11+
"radial-gradient(ellipse 30% 28% at 30% 50%, rgba(153,69,255,0.06) 0%, transparent 70%)",
12+
"radial-gradient(ellipse 30% 28% at 70% 50%, rgba(20,241,149,0.06) 0%, transparent 70%)",
13+
].join(", "),
14+
}}
15+
/>
16+
17+
{/* Large grid — purple (left) */}
18+
<div
19+
className="absolute inset-0 dark:opacity-60"
20+
style={{
21+
backgroundImage: `
22+
linear-gradient(to right, rgba(153,69,255,0.12) 1px, transparent 1px),
23+
linear-gradient(to bottom, rgba(153,69,255,0.12) 1px, transparent 1px)
24+
`,
25+
backgroundSize: "80px 80px",
26+
mask: "radial-gradient(ellipse 30% 35% at 30% 50%, black, transparent)",
27+
WebkitMask:
28+
"radial-gradient(ellipse 30% 35% at 30% 50%, black, transparent)",
29+
}}
30+
/>
31+
32+
{/* Large grid — green (right) */}
33+
<div
34+
className="absolute inset-0 dark:opacity-60"
35+
style={{
36+
backgroundImage: `
37+
linear-gradient(to right, rgba(20,241,149,0.12) 1px, transparent 1px),
38+
linear-gradient(to bottom, rgba(20,241,149,0.12) 1px, transparent 1px)
39+
`,
40+
backgroundSize: "80px 80px",
41+
mask: "radial-gradient(ellipse 30% 35% at 70% 50%, black, transparent)",
42+
WebkitMask:
43+
"radial-gradient(ellipse 30% 35% at 70% 50%, black, transparent)",
44+
}}
45+
/>
46+
47+
{/* Small grid — purple (left) */}
48+
<div
49+
className="absolute inset-0 dark:opacity-60"
50+
style={{
51+
backgroundImage: `
52+
linear-gradient(to right, rgba(153,69,255,0.06) 1px, transparent 1px),
53+
linear-gradient(to bottom, rgba(153,69,255,0.06) 1px, transparent 1px)
54+
`,
55+
backgroundSize: "16px 16px",
56+
mask: "radial-gradient(ellipse 30% 35% at 30% 50%, black, transparent)",
57+
WebkitMask:
58+
"radial-gradient(ellipse 30% 35% at 30% 50%, black, transparent)",
59+
}}
60+
/>
61+
62+
{/* Small grid — green (right) */}
63+
<div
64+
className="absolute inset-0 dark:opacity-60"
65+
style={{
66+
backgroundImage: `
67+
linear-gradient(to right, rgba(20,241,149,0.06) 1px, transparent 1px),
68+
linear-gradient(to bottom, rgba(20,241,149,0.06) 1px, transparent 1px)
69+
`,
70+
backgroundSize: "16px 16px",
71+
mask: "radial-gradient(ellipse 30% 35% at 70% 50%, black, transparent)",
72+
WebkitMask:
73+
"radial-gradient(ellipse 30% 35% at 70% 50%, black, transparent)",
74+
}}
75+
/>
76+
</div>
77+
);
78+
}
Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
"use client";
22

3-
import { SolanaProvider } from "@solana/react-hooks";
3+
import { ThemeProvider } from "next-themes";
4+
import { Toaster } from "sonner";
45
import { PropsWithChildren } from "react";
5-
6-
import { autoDiscover, createClient } from "@solana/client";
7-
8-
const client = createClient({
9-
endpoint: "https://api.devnet.solana.com",
10-
walletConnectors: autoDiscover(),
11-
});
6+
import { ClusterProvider } from "./cluster-context";
7+
import { WalletProvider } from "../lib/wallet/context";
8+
import { SolanaClientProvider } from "../lib/solana-client-context";
129

1310
export function Providers({ children }: PropsWithChildren) {
14-
return <SolanaProvider client={client}>{children}</SolanaProvider>;
11+
return (
12+
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
13+
<ClusterProvider>
14+
<SolanaClientProvider>
15+
<WalletProvider>{children}</WalletProvider>
16+
</SolanaClientProvider>
17+
<Toaster position="bottom-right" richColors />
18+
</ClusterProvider>
19+
</ThemeProvider>
20+
);
1521
}

0 commit comments

Comments
 (0)