Next.js - Solana Kit
Configura una integración mínima de wallet Solana en Next.js con Solana Kit.
Esta guía proporciona un ejemplo mínimo de implementación de funcionalidad de
wallet Solana en una aplicación Next.js usando @solana/kit
. Crearás un botón
de conexión de wallet y un componente para enviar transacciones.
Aplicación Nextjs Kit
Para un ejemplo más completo del uso de @solana/kit
en una aplicación React,
consulta el
Ejemplo de aplicación React
en el repositorio de Solana Kit.
Recursos
Requisitos previos
- Instalar Node.js
Crear proyecto Next.js
Crea un nuevo proyecto Next.js con shadcn para componentes de interfaz e instala las dependencias de Solana requeridas.
$npx shadcn@latest init
Navega a tu directorio de proyecto:
$cd <project-name>
Instalar componentes de UI
Instala los siguientes componentes de UI de shadcn:
$npx shadcn@latest add button dropdown-menu avatar
Instalar dependencias de Solana
Instala las siguientes dependencias de Solana:
$npm install @solana/kit @solana/react @wallet-standard/core @wallet-standard/react @solana-program/memo
Guía de implementación
Sigue los pasos a continuación y copia el código proporcionado a tu proyecto.
1. Crear contexto de Solana
Primero, crea un contexto de React que gestione todo el estado de la billetera para la aplicación.
Crea components/solana-provider.tsx
y añade el código proporcionado. Este
componente proveedor:
- Se conectará a los endpoints RPC de la devnet de Solana
- Filtrará las billeteras Solana disponibles instaladas en el navegador del usuario
- Hará seguimiento de qué billetera y cuenta está conectada actualmente
- Proporcionará el estado de la billetera a los componentes hijos
"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. Actualizar Layout
A continuación, envuelve toda la aplicación Next.js con el proveedor de Solana.
Actualiza app/layout.tsx
con el código proporcionado. Este paso:
- Importa el componente
SolanaProvider
- Envuelve los componentes hijos de la aplicación con el
SolanaProvider
- Asegura que todas las páginas y componentes tengan acceso a la funcionalidad de la billetera
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. Crear botón de conexión de billetera
Ahora construye el botón para conectar y desconectar billeteras.
Crea components/wallet-connect-button.tsx
y añade el código proporcionado.
Este botón desplegable:
- Muestra las billeteras disponibles cuando se hace clic
- Gestiona el flujo de conexión de la billetera utilizando el 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. Crear componente de envío de transacción
Crea un componente que envíe una transacción invocando el programa memo para añadir un mensaje a los registros de transacción.
El propósito de este componente es demostrar cómo enviar transacciones con la billetera conectada.
Crea components/memo-card.tsx
y añade el código proporcionado. Este
componente:
- Permite a los usuarios introducir un mensaje
- Crea una transacción de Solana con una instrucción que invoca el programa memo
- Solicita a la billetera conectada que firme y envíe la transacción
- Muestra un enlace para ver la transacción en 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. Actualizar la página de la aplicación
Finalmente, actualiza la página principal de la aplicación.
Actualiza app/page.tsx
con el código proporcionado. Esta página:
- Importa y utiliza los componentes
WalletConnectButton
yMemoCard
"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. Ejecutar la aplicación
Ahora ejecuta la aplicación para probar la integración de la billetera.
$npm run dev
Ten en cuenta que la billetera conectada debe estar configurada para conectarse al clúster devnet y financiada con SOL de devnet para enviar transacciones.
Is this page helpful?