Next.js - Solana Kit
Ρυθμίστε μια ελάχιστη ενσωμάτωση πορτοφολιού Solana στο Next.js με το Solana Kit.
Αυτός ο οδηγός παρέχει ένα ελάχιστο παράδειγμα υλοποίησης λειτουργικότητας
πορτοφολιού Solana σε μια εφαρμογή Next.js χρησιμοποιώντας το @solana/kit
. Θα
δημιουργήσετε ένα κουμπί σύνδεσης πορτοφολιού και ένα στοιχείο για την αποστολή
συναλλαγών.
Εφαρμογή Nextjs Kit
Για ένα πιο ολοκληρωμένο παράδειγμα χρήσης του @solana/kit
σε μια εφαρμογή
React, ανατρέξτε στο
Παράδειγμα εφαρμογής React
στο αποθετήριο του Solana Kit.
Πόροι
Προαπαιτούμενα
- Εγκαταστήστε το Node.js
Δημιουργία έργου Next.js
Δημιουργήστε ένα νέο έργο Next.js με το shadcn για στοιχεία διεπαφής χρήστη και εγκαταστήστε τις απαιτούμενες εξαρτήσεις Solana.
$npx shadcn@latest init
Μεταβείτε στον κατάλογο του έργου σας:
$cd <project-name>
Εγκατάσταση στοιχείων UI
Εγκαταστήστε τα ακόλουθα στοιχεία UI του shadcn:
$npx shadcn@latest add button dropdown-menu avatar
Εγκατάσταση εξαρτήσεων Solana
Εγκαταστήστε τις ακόλουθες εξαρτήσεις Solana:
$npm install @solana/kit @solana/react @wallet-standard/core @wallet-standard/react @solana-program/memo
Επισκόπηση υλοποίησης
Ακολουθήστε τα παρακάτω βήματα και αντιγράψτε τον παρεχόμενο κώδικα στο έργο σας.
1. Δημιουργία Solana Context
Πρώτα, δημιουργήστε ένα React Context που διαχειρίζεται ολόκληρη την κατάσταση του πορτοφολιού για την εφαρμογή.
Δημιουργήστε το components/solana-provider.tsx
και προσθέστε τον παρεχόμενο
κώδικα. Αυτό το component παρόχου θα:
- Συνδεθεί στα RPC endpoints του Solana devnet
- Φιλτράρει για διαθέσιμα πορτοφόλια Solana εγκατεστημένα στον browser του χρήστη
- Παρακολουθεί ποιο πορτοφόλι και λογαριασμός είναι συνδεδεμένος
- Παρέχει την κατάσταση του πορτοφολιού στα θυγατρικά components
"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
Στη συνέχεια, τυλίξτε ολόκληρη την εφαρμογή Next.js με τον πάροχο Solana.
Ενημερώστε το app/layout.tsx
με τον παρεχόμενο κώδικα. Αυτό το βήμα:
- Εισάγει το component
SolanaProvider
- Τυλίγει τα θυγατρικά components της εφαρμογής με το
SolanaProvider
- Διασφαλίζει ότι όλες οι σελίδες και τα components έχουν πρόσβαση στη λειτουργικότητα του πορτοφολιού
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. Δημιουργία Κουμπιού Σύνδεσης Πορτοφολιού
Τώρα δημιουργήστε το κουμπί για σύνδεση και αποσύνδεση πορτοφολιών.
Δημιουργήστε το components/wallet-connect-button.tsx
και προσθέστε τον
παρεχόμενο κώδικα. Αυτό το αναπτυσσόμενο κουμπί:
- Εμφανίζει τα διαθέσιμα πορτοφόλια όταν πατηθεί
- Διαχειρίζεται τη ροή σύνδεσης του πορτοφολιού χρησιμοποιώντας το 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. Δημιουργία Component Αποστολής Συναλλαγής
Δημιουργήστε ένα component που στέλνει μια συναλλαγή επικαλούμενο το πρόγραμμα memo για να προσθέσει ένα μήνυμα στα αρχεία καταγραφής συναλλαγών.
Ο σκοπός αυτού του component είναι να δείξει πώς να στέλνετε συναλλαγές με το συνδεδεμένο πορτοφόλι.
Δημιουργήστε το components/memo-card.tsx
και προσθέστε τον παρεχόμενο κώδικα.
Αυτό το component:
- Επιτρέπει στους χρήστες να εισάγουν ένα μήνυμα
- Δημιουργεί μια συναλλαγή Solana με μια εντολή που επικαλείται το πρόγραμμα memo
- Ζητά από το συνδεδεμένο πορτοφόλι να υπογράψει και να στείλει τη συναλλαγή
- Εμφανίζει έναν σύνδεσμο για προβολή της συναλλαγής στον 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
Τέλος, ενημερώστε την κύρια σελίδα της εφαρμογής.
Ενημερώστε το app/page.tsx
με τον παρεχόμενο κώδικα. Αυτή η σελίδα:
- Εισάγει και χρησιμοποιεί τα components
WalletConnectButton
καιMemoCard
"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. Εκτέλεση της εφαρμογής
Τώρα εκτελέστε την εφαρμογή για να δοκιμάσετε την ενσωμάτωση του πορτοφολιού.
$npm run dev
Σημειώστε ότι το συνδεδεμένο πορτοφόλι πρέπει να είναι ρυθμισμένο για σύνδεση στο cluster devnet και χρηματοδοτημένο με devnet SOL για να στέλνει συναλλαγές.
Is this page helpful?