Next.js - Solana Kit
Stel een minimale Solana wallet-integratie op in Next.js met Solana Kit.
Deze gids biedt een minimaal voorbeeld van het implementeren van Solana
wallet-functionaliteit in een Next.js-applicatie met behulp van @solana/kit
.
Je maakt een knop om je wallet te verbinden en een component om transacties te
verzenden.
Nextjs Kit Applicatie
Voor een uitgebreider voorbeeld van het gebruik van @solana/kit
in een
React-applicatie, raadpleeg het
React App Voorbeeld
in de Solana Kit repository.
Bronnen
Vereisten
- Installeer Node.js
Next.js-project aanmaken
Maak een nieuw Next.js-project aan met shadcn voor UI-componenten en installeer de vereiste Solana-afhankelijkheden.
$npx shadcn@latest init
Navigeer naar je projectmap:
$cd <project-name>
UI-componenten installeren
Installeer de volgende shadcn UI-componenten:
$npx shadcn@latest add button dropdown-menu avatar
Solana-afhankelijkheden installeren
Installeer de volgende Solana-afhankelijkheden:
$npm install @solana/kit @solana/react @wallet-standard/core @wallet-standard/react @solana-program/memo
Implementatie walkthrough
Volg de onderstaande stappen en kopieer de aangeleverde code naar je project.
1. Solana Context maken
Maak eerst een React Context die de volledige wallet-status voor de applicatie beheert.
Maak components/solana-provider.tsx
en voeg de gegeven code toe. Deze provider
component zal:
- Verbinding maken met Solana's devnet RPC-eindpunten
- Filteren op beschikbare Solana wallets geïnstalleerd in de browser van de gebruiker
- Bijhouden welke wallet en account momenteel verbonden is
- Wallet-status beschikbaar maken voor onderliggende componenten
"use client";import React, { createContext, useContext, useState, useMemo } from "react";import {useWallets,type UiWallet,type UiWalletAccount} from "@wallet-standard/react";import { createSolanaRpc, createSolanaRpcSubscriptions } from "@solana/kit";import { StandardConnect } from "@wallet-standard/core";// Create RPC connectionconst RPC_ENDPOINT = "https://api.devnet.solana.com";const WS_ENDPOINT = "wss://api.devnet.solana.com";const chain = "solana:devnet";const rpc = createSolanaRpc(RPC_ENDPOINT);const ws = createSolanaRpcSubscriptions(WS_ENDPOINT);interface SolanaContextState {// RPCrpc: ReturnType<typeof createSolanaRpc>;ws: ReturnType<typeof createSolanaRpcSubscriptions>;chain: typeof chain;// Wallet Statewallets: UiWallet[];selectedWallet: UiWallet | null;selectedAccount: UiWalletAccount | null;isConnected: boolean;// Wallet ActionssetWalletAndAccount: (wallet: UiWallet | null,account: UiWalletAccount | null) => void;}const SolanaContext = createContext<SolanaContextState | undefined>(undefined);export function useSolana() {const context = useContext(SolanaContext);if (!context) {throw new Error("useSolana must be used within a SolanaProvider");}return context;}export function SolanaProvider({ children }: { children: React.ReactNode }) {const allWallets = useWallets();// Filter for Solana wallets only that support signAndSendTransactionconst wallets = useMemo(() => {return allWallets.filter((wallet) =>wallet.chains?.some((c) => c.startsWith("solana:")) &&wallet.features.includes(StandardConnect) &&wallet.features.includes("solana:signAndSendTransaction"));}, [allWallets]);// State managementconst [selectedWallet, setSelectedWallet] = useState<UiWallet | null>(null);const [selectedAccount, setSelectedAccount] =useState<UiWalletAccount | null>(null);// Check if connected (account must exist in the wallet's accounts)const isConnected = useMemo(() => {if (!selectedAccount || !selectedWallet) return false;// Find the wallet and check if it still has this accountconst currentWallet = wallets.find((w) => w.name === selectedWallet.name);return !!(currentWallet &¤tWallet.accounts.some((acc) => acc.address === selectedAccount.address));}, [selectedAccount, selectedWallet, wallets]);const setWalletAndAccount = (wallet: UiWallet | null,account: UiWalletAccount | null) => {setSelectedWallet(wallet);setSelectedAccount(account);};// Create context valueconst contextValue = useMemo<SolanaContextState>(() => ({// Static RPC valuesrpc,ws,chain,// Dynamic wallet valueswallets,selectedWallet,selectedAccount,isConnected,setWalletAndAccount}),[wallets, selectedWallet, selectedAccount, isConnected]);return (<SolanaContext.Provider value={contextValue}>{children}</SolanaContext.Provider>);}
2. Layout bijwerken
Omsluit vervolgens de gehele Next.js-applicatie met de Solana provider.
Werk app/layout.tsx
bij met de gegeven code. Deze stap:
- Importeert de
SolanaProvider
component - Omsluit de onderliggende componenten van de applicatie met de
SolanaProvider
- Zorgt ervoor dat alle pagina's en componenten toegang hebben tot wallet-functionaliteit
import { SolanaProvider } from "@/components/solana-provider";import "./globals.css";export default function RootLayout({children}: Readonly<{children: React.ReactNode;}>) {return (<html lang="en"><body><SolanaProvider>{children}</SolanaProvider></body></html>);}
3. Wallet Connect-knop maken
Bouw nu de knop om wallets te verbinden en te ontkoppelen.
Maak components/wallet-connect-button.tsx
en voeg de gegeven code toe. Deze
dropdown-knop:
- Toont beschikbare wallets wanneer erop geklikt wordt
- Verwerkt de wallet-verbindingsflow met behulp van de Wallet Standard
"use client";import { useState } from "react";import { useSolana } from "@/components/solana-provider";import { Button } from "@/components/ui/button";import {DropdownMenu,DropdownMenuContent,DropdownMenuItem,DropdownMenuLabel,DropdownMenuSeparator,DropdownMenuTrigger} from "@/components/ui/dropdown-menu";import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";import { ChevronDown, Wallet, LogOut } from "lucide-react";import {useConnect,useDisconnect,type UiWallet} from "@wallet-standard/react";function truncateAddress(address: string): string {return `${address.slice(0, 4)}...${address.slice(-4)}`;}function WalletIcon({wallet,className}: {wallet: UiWallet;className?: string;}) {return (<Avatar className={className}>{wallet.icon && (<AvatarImage src={wallet.icon} alt={`${wallet.name} icon`} />)}<AvatarFallback>{wallet.name.slice(0, 2).toUpperCase()}</AvatarFallback></Avatar>);}function WalletMenuItem({wallet,onConnect}: {wallet: UiWallet;onConnect: () => void;}) {const { setWalletAndAccount } = useSolana();const [isConnecting, connect] = useConnect(wallet);const handleConnect = async () => {if (isConnecting) return;try {const accounts = await connect();if (accounts && accounts.length > 0) {const account = accounts[0];setWalletAndAccount(wallet, account);onConnect();}} catch (err) {console.error(`Failed to connect ${wallet.name}:`, err);}};return (<buttonclassName="flex w-full items-center justify-between px-2 py-1.5 text-sm outline-none hover:bg-accent focus:bg-accent disabled:pointer-events-none disabled:opacity-50"onClick={handleConnect}disabled={isConnecting}><div className="flex items-center gap-2"><WalletIcon wallet={wallet} className="h-6 w-6" /><span className="font-medium">{wallet.name}</span></div></button>);}function DisconnectButton({wallet,onDisconnect}: {wallet: UiWallet;onDisconnect: () => void;}) {const { setWalletAndAccount } = useSolana();const [isDisconnecting, disconnect] = useDisconnect(wallet);const handleDisconnect = async () => {try {await disconnect();setWalletAndAccount(null, null);onDisconnect();} catch (err) {console.error("Failed to disconnect wallet:", err);}};return (<DropdownMenuItemclassName="text-destructive focus:text-destructive cursor-pointer"onClick={handleDisconnect}disabled={isDisconnecting}><LogOut className="mr-2 h-4 w-4" />Disconnect</DropdownMenuItem>);}export function WalletConnectButton() {const { wallets, selectedWallet, selectedAccount, isConnected } = useSolana();const [dropdownOpen, setDropdownOpen] = useState(false);return (<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}><DropdownMenuTrigger asChild><Button variant="outline" className="min-w-[140px] justify-between">{isConnected && selectedWallet && selectedAccount ? (<><div className="flex items-center gap-2"><WalletIcon wallet={selectedWallet} className="h-4 w-4" /><span className="font-mono text-sm">{truncateAddress(selectedAccount.address)}</span></div><ChevronDown className="ml-2 h-4 w-4" /></>) : (<><Wallet className="mr-2 h-4 w-4" /><span>Connect Wallet</span><ChevronDown className="ml-2 h-4 w-4" /></>)}</Button></DropdownMenuTrigger><DropdownMenuContent align="end" className="w-[280px]">{wallets.length === 0 ? (<p className="text-sm text-muted-foreground p-3 text-center">No wallets detected</p>) : (<>{!isConnected ? (<><DropdownMenuLabel>Available Wallets</DropdownMenuLabel><DropdownMenuSeparator />{wallets.map((wallet, index) => (<WalletMenuItemkey={`${wallet.name}-${index}`}wallet={wallet}onConnect={() => setDropdownOpen(false)}/>))}</>) : (selectedWallet &&selectedAccount && (<><DropdownMenuLabel>Connected Wallet</DropdownMenuLabel><DropdownMenuSeparator /><div className="px-2 py-1.5"><div className="flex items-center gap-2"><WalletIcon wallet={selectedWallet} className="h-6 w-6" /><div className="flex flex-col"><span className="text-sm font-medium">{selectedWallet.name}</span><span className="text-xs text-muted-foreground font-mono">{truncateAddress(selectedAccount.address)}</span></div></div></div><DropdownMenuSeparator /><DisconnectButtonwallet={selectedWallet}onDisconnect={() => setDropdownOpen(false)}/></>))}</>)}</DropdownMenuContent></DropdownMenu>);}
4. Transactiecomponent maken
Maak een component dat een transactie verzendt die het memo-programma aanroept om een bericht toe te voegen aan de transactielogs.
Het doel van dit component is om te demonstreren hoe je transacties kunt verzenden met de verbonden wallet.
Maak components/memo-card.tsx
en voeg de meegeleverde code toe. Dit component:
- Stelt gebruikers in staat om een bericht in te voeren
- Maakt een Solana-transactie met een instructie die het memo-programma aanroept
- Vraagt de verbonden wallet om de transactie te ondertekenen en te verzenden
- Toont een link om de transactie te bekijken op Solana Explorer
"use client";import { useState } from "react";import { useSolana } from "@/components/solana-provider";import { useWalletAccountTransactionSendingSigner } from "@solana/react";import { type UiWalletAccount } from "@wallet-standard/react";import {pipe,createTransactionMessage,appendTransactionMessageInstruction,setTransactionMessageFeePayerSigner,setTransactionMessageLifetimeUsingBlockhash,signAndSendTransactionMessageWithSigners,getBase58Decoder,type Signature} from "@solana/kit";import { getAddMemoInstruction } from "@solana-program/memo";// Component that only renders when wallet is connectedfunction ConnectedMemoCard({ account }: { account: UiWalletAccount }) {const { rpc, chain } = useSolana();const [isLoading, setIsLoading] = useState(false);const [memoText, setMemoText] = useState("");const [txSignature, setTxSignature] = useState("");const signer = useWalletAccountTransactionSendingSigner(account, chain);const sendMemo = async () => {if (!signer) return;setIsLoading(true);try {const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: "confirmed" }).send();const memoInstruction = getAddMemoInstruction({ memo: memoText });const message = pipe(createTransactionMessage({ version: 0 }),(m) => setTransactionMessageFeePayerSigner(signer, m),(m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),(m) => appendTransactionMessageInstruction(memoInstruction, m));const signature = await signAndSendTransactionMessageWithSigners(message);const signatureStr = getBase58Decoder().decode(signature) as Signature;setTxSignature(signatureStr);setMemoText("");} catch (error) {console.error("Memo failed:", error);} finally {setIsLoading(false);}};return (<div className="space-y-4"><div><label className="block text-sm mb-1">Memo Message</label><textareavalue={memoText}onChange={(e) => setMemoText(e.target.value)}placeholder="Enter your memo message"className="w-full p-2 border rounded min-h-[100px]"maxLength={566}/></div><buttononClick={sendMemo}disabled={isLoading || !memoText.trim()}className="w-full p-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-400">{isLoading ? "Sending..." : "Send Memo"}</button>{txSignature && (<div className="p-2 border rounded text-sm"><p className="mb-1">Memo Sent</p><ahref={`https://explorer.solana.com/tx/${txSignature}?cluster=devnet`}target="_blank"rel="noopener noreferrer"className="text-blue-500 hover:underline">View on Solana Explorer →</a></div>)}</div>);}// Main memo componentexport function MemoCard() {const { selectedAccount, isConnected } = useSolana();return (<div className="space-y-4 p-4 border rounded-lg"><h3 className="text-lg font-semibold">Send Memo</h3>{isConnected && selectedAccount ? (<ConnectedMemoCard account={selectedAccount} />) : (<p className="text-gray-500 text-center py-4">Connect your wallet to send a memo</p>)}</div>);}
5. App-pagina bijwerken
Werk ten slotte de hoofdpagina van de app bij.
Werk app/page.tsx
bij met de meegeleverde code. Deze pagina:
- Importeert en gebruikt de
WalletConnectButton
enMemoCard
componenten
"use client";import { WalletConnectButton } from "@/components/wallet-connect-button";import { MemoCard } from "@/components/memo-card";export default function Home() {return (<div className="min-h-screen flex items-center justify-center p-4"><div className="w-full max-w-md bg-card rounded-lg border shadow-lg p-6 space-y-6"><div className="flex justify-center"><WalletConnectButton /></div><MemoCard /></div></div>);}
6. De applicatie uitvoeren
Voer nu de applicatie uit om de wallet-integratie te testen.
$npm run dev
Let op: de verbonden wallet moet geconfigureerd zijn om verbinding te maken met de devnet-cluster en gefinancierd zijn met devnet SOL om transacties te kunnen verzenden.
Is this page helpful?