Next.js - Solana Kit
Skonfiguruj minimalną integrację portfela Solana w Next.js za pomocą Solana Kit.
Ten przewodnik przedstawia minimalny przykład implementacji funkcjonalności
portfela Solana w aplikacji Next.js przy użyciu @solana/kit
. Utworzysz
przycisk do łączenia portfela oraz komponent do wysyłania transakcji.
Aplikacja Nextjs Kit
Bardziej kompleksowy przykład użycia @solana/kit
w aplikacji React znajdziesz
w
Przykładzie aplikacji React
w repozytorium Solana Kit.
Zasoby
Wymagania wstępne
- Zainstaluj Node.js
Utwórz projekt Next.js
Utwórz nowy projekt Next.js z shadcn dla komponentów interfejsu użytkownika i zainstaluj wymagane zależności Solana.
$npx shadcn@latest init
Przejdź do katalogu swojego projektu:
$cd <project-name>
Zainstaluj komponenty interfejsu użytkownika
Zainstaluj następujące komponenty interfejsu użytkownika shadcn:
$npx shadcn@latest add button dropdown-menu avatar
Zainstaluj zależności Solana
Zainstaluj następujące zależności Solana:
$npm install @solana/kit @solana/react @wallet-standard/core @wallet-standard/react @solana-program/memo
Przewodnik implementacji
Postępuj zgodnie z poniższymi krokami i skopiuj podany kod do swojego projektu.
1. Utwórz kontekst Solana
Najpierw utwórz kontekst React, który zarządza całym stanem portfela dla aplikacji.
Utwórz components/solana-provider.tsx
i dodaj dostarczony kod. Ten komponent
dostarczający będzie:
- Łączył się z punktami końcowymi RPC devnet Solana
- Filtrował dostępne portfele Solana zainstalowane w przeglądarce użytkownika
- Śledził, który portfel i konto są aktualnie połączone
- Udostępniał stan portfela komponentom potomnym
"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. Zaktualizuj układ
Następnie owiń całą aplikację Next.js dostarczającym Solana.
Zaktualizuj app/layout.tsx
za pomocą dostarczonego kodu. Ten krok:
- Importuje komponent
SolanaProvider
- Obejmuje komponenty potomne aplikacji za pomocą
SolanaProvider
- Zapewnia dostęp do funkcjonalności portfela na wszystkich stronach i w komponentach
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. Utwórz przycisk połączenia portfela
Teraz zbuduj przycisk do łączenia i rozłączania portfeli.
Utwórz components/wallet-connect-button.tsx
i dodaj dostarczony kod. Ten
przycisk rozwijany:
- Wyświetla dostępne portfele po kliknięciu
- Obsługuje proces łączenia portfela za pomocą 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. Utwórz komponent wysyłania transakcji
Utwórz komponent, który wysyła transakcję wywołującą program memo, aby dodać wiadomość do logów tłumaczeń.
Celem tego komponentu jest pokazanie, jak wysyłać transakcje za pomocą podłączonego portfela.
Utwórz components/memo-card.tsx
i dodaj dostarczony kod. Ten komponent:
- Pozwala użytkownikom wprowadzać wiadomości
- Tworzy transakcję Solana z instrukcją wywołującą program memo
- Prosi podłączony portfel o podpisanie i wysłanie transakcji
- Wyświetla link do podglądu transakcji w 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. Zaktualizuj stronę aplikacji
Na koniec zaktualizuj główną stronę aplikacji.
Zaktualizuj app/page.tsx
za pomocą dostarczonego kodu. Ta strona:
- Importuje i używa komponentów
WalletConnectButton
iMemoCard
"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. Uruchom aplikację
Teraz uruchom aplikację, aby przetestować integrację portfela.
$npm run dev
Zwróć uwagę, że podłączony portfel musi być skonfigurowany do połączenia z klastrem devnet i zasilony SOL z devnet, aby wysyłać transakcje.
Is this page helpful?