diff --git a/kit/nextjs-anchor/README.md b/kit/nextjs-anchor/README.md index 888281e4..657964b9 100644 --- a/kit/nextjs-anchor/README.md +++ b/kit/nextjs-anchor/README.md @@ -1,6 +1,6 @@ # nextjs-anchor -Next.js starter with Tailwind CSS, `@solana/react-hooks`, and an Anchor vault program example. +Next.js starter with Tailwind CSS, `@solana/kit`, and an Anchor vault program example. ## Getting Started @@ -9,43 +9,90 @@ npx -y create-solana-dapp@latest -t solana-foundation/templates/kit/nextjs-ancho ``` ```shell -npm install # Builds program and generates client automatically +npm install +npm run setup # Builds the Anchor program and generates the TypeScript client npm run dev ``` -Open [http://localhost:3000](http://localhost:3000), connect your wallet, and interact with the vault on devnet. +Open [http://localhost:3000](http://localhost:3000), connect your wallet, and interact with the vault. ## What's Included -- **Wallet connection** via `@solana/react-hooks` with auto-discovery -- **SOL Vault program** - deposit and withdraw SOL from a personal PDA vault -- **Codama-generated client** - type-safe program interactions using `@solana/kit` -- **Tailwind CSS v4** with light/dark mode +- **Wallet connection** via wallet-standard with auto-discovery and dropdown UI +- **Cluster switching** — devnet, testnet, mainnet, and localnet from the header +- **Wallet balance** display with airdrop button (devnet/testnet/localnet) +- **SOL Vault program** — deposit and withdraw SOL from a personal PDA vault +- **Toast notifications** with explorer links for every transaction +- **Error handling** — human-readable messages for common Solana and program errors +- **Codama-generated client** — type-safe program interactions using `@solana/kit` +- **Tailwind CSS v4** with light/dark mode toggle ## Stack -| Layer | Technology | -| -------------- | --------------------------------------- | -| Frontend | Next.js 16, React 19, TypeScript | -| Styling | Tailwind CSS v4 | -| Solana Client | `@solana/client`, `@solana/react-hooks` | -| Program Client | Codama-generated, `@solana/kit` | -| Program | Anchor (Rust) | +| Layer | Technology | +| -------------- | -------------------------------- | +| Frontend | Next.js 16, React 19, TypeScript | +| Styling | Tailwind CSS v4 | +| Solana Client | `@solana/kit`, wallet-standard | +| Program Client | Codama-generated, `@solana/kit` | +| Program | Anchor (Rust) | ## Project Structure ``` ├── app/ │ ├── components/ -│ │ ├── providers.tsx # Solana client setup -│ │ └── vault-card.tsx # Vault deposit/withdraw UI -│ ├── generated/vault/ # Codama-generated program client -│ └── page.tsx # Main page -├── anchor/ # Anchor workspace -│ └── programs/vault/ # Vault program (Rust) -└── codama.json # Codama client generation config +│ │ ├── cluster-context.tsx # Cluster state (React context + localStorage) +│ │ ├── cluster-select.tsx # Cluster switcher dropdown +│ │ ├── grid-background.tsx # Solana-branded decorative grid +│ │ ├── providers.tsx # Wallet + theme providers +│ │ ├── theme-toggle.tsx # Light/dark mode toggle +│ │ ├── vault-card.tsx # Vault deposit/withdraw UI +│ │ └── wallet-button.tsx # Wallet connect/disconnect dropdown +│ ├── generated/vault/ # Codama-generated program client +│ ├── lib/ +│ │ ├── wallet/ # Wallet-standard connection layer +│ │ │ ├── types.ts # Wallet types +│ │ │ ├── standard.ts # Wallet discovery + session creation +│ │ │ ├── signer.ts # WalletSession → TransactionSigner +│ │ │ └── context.tsx # WalletProvider + useWallet() hook +│ │ ├── hooks/ +│ │ │ ├── use-balance.ts # SWR-based balance fetching +│ │ │ └── use-send-transaction.ts # Transaction send with loading state +│ │ ├── cluster.ts # Cluster endpoints + RPC factory +│ │ ├── lamports.ts # SOL/lamports conversion +│ │ ├── send-transaction.ts # Transaction build + sign + send pipeline +│ │ ├── errors.ts # Transaction error parsing +│ │ └── explorer.ts # Explorer URL builder + address helpers +│ └── page.tsx # Main page +├── anchor/ # Anchor workspace +│ └── programs/vault/ # Vault program (Rust) +└── codama.json # Codama client generation config ``` +## Local Development + +To test against a local validator instead of devnet: + +1. **Start a local validator** + + ```bash + solana-test-validator + ``` + +2. **Deploy the program locally** + + ```bash + solana config set --url localhost + cd anchor + anchor build + anchor deploy + cd .. + npm run codama:js # Regenerate client with local program ID + ``` + +3. **Switch to localnet** in the app using the cluster selector in the header. + ## Deploy Your Own Vault 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 ## Learn More -- [Solana Docs](https://solana.com/docs) - core concepts and guides -- [Anchor Docs](https://www.anchor-lang.com/docs) - program development framework -- [Deploying Programs](https://solana.com/docs/programs/deploying) - deployment guide -- [framework-kit](https://github.com/solana-foundation/framework-kit) - the React hooks used here -- [Codama](https://github.com/codama-idl/codama) - client generation from IDL +- [Solana Docs](https://solana.com/docs) — core concepts and guides +- [Anchor Docs](https://www.anchor-lang.com/docs/introduction) — program development framework +- [Deploying Programs](https://solana.com/docs/programs/deploying) — deployment guide +- [@solana/kit](https://github.com/anza-xyz/kit) — Solana JavaScript SDK +- [Codama](https://github.com/codama-idl/codama) — client generation from IDL diff --git a/kit/nextjs-anchor/app/components/cluster-context.tsx b/kit/nextjs-anchor/app/components/cluster-context.tsx new file mode 100644 index 00000000..fe23a8a9 --- /dev/null +++ b/kit/nextjs-anchor/app/components/cluster-context.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { + createContext, + useContext, + useState, + useCallback, + type ReactNode, +} from "react"; +import type { ClusterMoniker } from "../lib/cluster"; +import { CLUSTERS } from "../lib/cluster"; +import { getExplorerUrl } from "../lib/explorer"; + +type ClusterContextValue = { + cluster: ClusterMoniker; + setCluster: (cluster: ClusterMoniker) => void; + getExplorerUrl: (path: string) => string; +}; + +const ClusterContext = createContext(null); + +const STORAGE_KEY = "solana-cluster"; +function getInitialCluster(): ClusterMoniker { + if (typeof window === "undefined") return "devnet"; + const stored = localStorage.getItem(STORAGE_KEY); + if (stored && CLUSTERS.includes(stored as ClusterMoniker)) { + return stored as ClusterMoniker; + } + return "devnet"; +} + +export { CLUSTERS }; + +export function ClusterProvider({ children }: { children: ReactNode }) { + const [cluster, setClusterState] = + useState(getInitialCluster); + + const setCluster = useCallback((c: ClusterMoniker) => { + setClusterState(c); + localStorage.setItem(STORAGE_KEY, c); + }, []); + + const explorerUrl = useCallback( + (path: string) => getExplorerUrl(path, cluster), + [cluster] + ); + + return ( + + {children} + + ); +} + +export function useCluster() { + const ctx = useContext(ClusterContext); + if (!ctx) throw new Error("useCluster must be used within ClusterProvider"); + return ctx; +} diff --git a/kit/nextjs-anchor/app/components/cluster-select.tsx b/kit/nextjs-anchor/app/components/cluster-select.tsx new file mode 100644 index 00000000..6b05e265 --- /dev/null +++ b/kit/nextjs-anchor/app/components/cluster-select.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { useCluster, CLUSTERS } from "./cluster-context"; + +export function ClusterSelect() { + const { cluster, setCluster } = useCluster(); + const [isOpen, setIsOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) { + setIsOpen(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + return ( +
+ + + {isOpen && ( +
+
+ {CLUSTERS.map((c) => ( + + ))} +
+
+ )} +
+ ); +} diff --git a/kit/nextjs-anchor/app/components/grid-background.tsx b/kit/nextjs-anchor/app/components/grid-background.tsx new file mode 100644 index 00000000..2fb9fe7a --- /dev/null +++ b/kit/nextjs-anchor/app/components/grid-background.tsx @@ -0,0 +1,78 @@ +"use client"; + +export function GridBackground() { + return ( +