Next.js - Solana Kit

Ρυθμίστε μια ελάχιστη ενσωμάτωση πορτοφολιού Solana στο Next.js με το Solana Kit.

Αυτός ο οδηγός παρέχει ένα ελάχιστο παράδειγμα υλοποίησης λειτουργικότητας πορτοφολιού Solana σε μια εφαρμογή Next.js χρησιμοποιώντας το @solana/kit. Θα δημιουργήσετε ένα κουμπί σύνδεσης πορτοφολιού και ένα στοιχείο για την αποστολή συναλλαγών.

Εφαρμογή Nextjs KitΕφαρμογή Nextjs Kit

Για ένα πιο ολοκληρωμένο παράδειγμα χρήσης του @solana/kit σε μια εφαρμογή React, ανατρέξτε στο Παράδειγμα εφαρμογής React στο αποθετήριο του Solana Kit.

Πόροι

Προαπαιτούμενα

  • Εγκαταστήστε το Node.js

Δημιουργία έργου Next.js

Δημιουργήστε ένα νέο έργο Next.js με το shadcn για στοιχεία διεπαφής χρήστη και εγκαταστήστε τις απαιτούμενες εξαρτήσεις Solana.

Terminal
$
npx shadcn@latest init

Μεταβείτε στον κατάλογο του έργου σας:

Terminal
$
cd <project-name>

Εγκατάσταση στοιχείων UI

Εγκαταστήστε τα ακόλουθα στοιχεία UI του shadcn:

Terminal
$
npx shadcn@latest add button dropdown-menu avatar

Εγκατάσταση εξαρτήσεων Solana

Εγκαταστήστε τις ακόλουθες εξαρτήσεις Solana:

Terminal
$
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
components/solana-provider.tsx
"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 connection
const 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 {
// RPC
rpc: ReturnType<typeof createSolanaRpc>;
ws: ReturnType<typeof createSolanaRpcSubscriptions>;
chain: typeof chain;
// Wallet State
wallets: UiWallet[];
selectedWallet: UiWallet | null;
selectedAccount: UiWalletAccount | null;
isConnected: boolean;
// Wallet Actions
setWalletAndAccount: (
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 signAndSendTransaction
const wallets = useMemo(() => {
return allWallets.filter(
(wallet) =>
wallet.chains?.some((c) => c.startsWith("solana:")) &&
wallet.features.includes(StandardConnect) &&
wallet.features.includes("solana:signAndSendTransaction")
);
}, [allWallets]);
// State management
const [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 account
const currentWallet = wallets.find((w) => w.name === selectedWallet.name);
return !!(
currentWallet &&
currentWallet.accounts.some(
(acc) => acc.address === selectedAccount.address
)
);
}, [selectedAccount, selectedWallet, wallets]);
const setWalletAndAccount = (
wallet: UiWallet | null,
account: UiWalletAccount | null
) => {
setSelectedWallet(wallet);
setSelectedAccount(account);
};
// Create context value
const contextValue = useMemo<SolanaContextState>(
() => ({
// Static RPC values
rpc,
ws,
chain,
// Dynamic wallet values
wallets,
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 έχουν πρόσβαση στη λειτουργικότητα του πορτοφολιού
app/layout.tsx
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
components/wallet-connect-button.tsx
"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 (
<button
className="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 (
<DropdownMenuItem
className="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) => (
<WalletMenuItem
key={`${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 />
<DisconnectButton
wallet={selectedWallet}
onDisconnect={() => setDropdownOpen(false)}
/>
</>
)
)}
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}

4. Δημιουργία Component Αποστολής Συναλλαγής

Δημιουργήστε ένα component που στέλνει μια συναλλαγή επικαλούμενο το πρόγραμμα memo για να προσθέσει ένα μήνυμα στα αρχεία καταγραφής συναλλαγών.

Ο σκοπός αυτού του component είναι να δείξει πώς να στέλνετε συναλλαγές με το συνδεδεμένο πορτοφόλι.

Δημιουργήστε το components/memo-card.tsx και προσθέστε τον παρεχόμενο κώδικα. Αυτό το component:

  • Επιτρέπει στους χρήστες να εισάγουν ένα μήνυμα
  • Δημιουργεί μια συναλλαγή Solana με μια εντολή που επικαλείται το πρόγραμμα memo
  • Ζητά από το συνδεδεμένο πορτοφόλι να υπογράψει και να στείλει τη συναλλαγή
  • Εμφανίζει έναν σύνδεσμο για προβολή της συναλλαγής στον Solana Explorer
components/memo-card.tsx
"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 connected
function 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>
<textarea
value={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>
<button
onClick={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>
<a
href={`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 component
export 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
app/page.tsx
"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. Εκτέλεση της εφαρμογής

Τώρα εκτελέστε την εφαρμογή για να δοκιμάσετε την ενσωμάτωση του πορτοφολιού.

Terminal
$
npm run dev

Σημειώστε ότι το συνδεδεμένο πορτοφόλι πρέπει να είναι ρυθμισμένο για σύνδεση στο cluster devnet και χρηματοδοτημένο με devnet SOL για να στέλνει συναλλαγές.

1. Δημιουργία Solana Context

Πρώτα, δημιουργήστε ένα React Context που διαχειρίζεται ολόκληρη την κατάσταση του πορτοφολιού για την εφαρμογή.

Δημιουργήστε το components/solana-provider.tsx και προσθέστε τον παρεχόμενο κώδικα. Αυτό το component παρόχου θα:

  • Συνδεθεί στα RPC endpoints του Solana devnet
  • Φιλτράρει για διαθέσιμα πορτοφόλια Solana εγκατεστημένα στον browser του χρήστη
  • Παρακολουθεί ποιο πορτοφόλι και λογαριασμός είναι συνδεδεμένος
  • Παρέχει την κατάσταση του πορτοφολιού στα θυγατρικά components

2. Ενημέρωση του Layout

Στη συνέχεια, τυλίξτε ολόκληρη την εφαρμογή Next.js με τον πάροχο Solana.

Ενημερώστε το app/layout.tsx με τον παρεχόμενο κώδικα. Αυτό το βήμα:

  • Εισάγει το component SolanaProvider
  • Τυλίγει τα θυγατρικά components της εφαρμογής με το SolanaProvider
  • Διασφαλίζει ότι όλες οι σελίδες και τα components έχουν πρόσβαση στη λειτουργικότητα του πορτοφολιού

3. Δημιουργία Κουμπιού Σύνδεσης Πορτοφολιού

Τώρα δημιουργήστε το κουμπί για σύνδεση και αποσύνδεση πορτοφολιών.

Δημιουργήστε το components/wallet-connect-button.tsx και προσθέστε τον παρεχόμενο κώδικα. Αυτό το αναπτυσσόμενο κουμπί:

  • Εμφανίζει τα διαθέσιμα πορτοφόλια όταν πατηθεί
  • Διαχειρίζεται τη ροή σύνδεσης του πορτοφολιού χρησιμοποιώντας το Wallet Standard

4. Δημιουργία Component Αποστολής Συναλλαγής

Δημιουργήστε ένα component που στέλνει μια συναλλαγή επικαλούμενο το πρόγραμμα memo για να προσθέσει ένα μήνυμα στα αρχεία καταγραφής συναλλαγών.

Ο σκοπός αυτού του component είναι να δείξει πώς να στέλνετε συναλλαγές με το συνδεδεμένο πορτοφόλι.

Δημιουργήστε το components/memo-card.tsx και προσθέστε τον παρεχόμενο κώδικα. Αυτό το component:

  • Επιτρέπει στους χρήστες να εισάγουν ένα μήνυμα
  • Δημιουργεί μια συναλλαγή Solana με μια εντολή που επικαλείται το πρόγραμμα memo
  • Ζητά από το συνδεδεμένο πορτοφόλι να υπογράψει και να στείλει τη συναλλαγή
  • Εμφανίζει έναν σύνδεσμο για προβολή της συναλλαγής στον Solana Explorer

5. Ενημέρωση της σελίδας App

Τέλος, ενημερώστε την κύρια σελίδα της εφαρμογής.

Ενημερώστε το app/page.tsx με τον παρεχόμενο κώδικα. Αυτή η σελίδα:

  • Εισάγει και χρησιμοποιεί τα components WalletConnectButton και MemoCard

6. Εκτέλεση της εφαρμογής

Τώρα εκτελέστε την εφαρμογή για να δοκιμάσετε την ενσωμάτωση του πορτοφολιού.

Terminal
$
npm run dev

Σημειώστε ότι το συνδεδεμένο πορτοφόλι πρέπει να είναι ρυθμισμένο για σύνδεση στο cluster devnet και χρηματοδοτημένο με devnet SOL για να στέλνει συναλλαγές.

layout.tsx
page.tsx
solana-provider.tsx
package.json
"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 connection
const 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 {
// RPC
rpc: ReturnType<typeof createSolanaRpc>;
ws: ReturnType<typeof createSolanaRpcSubscriptions>;
chain: typeof chain;
// Wallet State
wallets: UiWallet[];
selectedWallet: UiWallet | null;
selectedAccount: UiWalletAccount | null;
isConnected: boolean;
// Wallet Actions
setWalletAndAccount: (
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 signAndSendTransaction
const wallets = useMemo(() => {
return allWallets.filter(
(wallet) =>
wallet.chains?.some((c) => c.startsWith("solana:")) &&
wallet.features.includes(StandardConnect) &&
wallet.features.includes("solana:signAndSendTransaction")
);
}, [allWallets]);
// State management
const [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 account
const currentWallet = wallets.find((w) => w.name === selectedWallet.name);
return !!(
currentWallet &&
currentWallet.accounts.some(
(acc) => acc.address === selectedAccount.address
)
);
}, [selectedAccount, selectedWallet, wallets]);
const setWalletAndAccount = (
wallet: UiWallet | null,
account: UiWalletAccount | null
) => {
setSelectedWallet(wallet);
setSelectedAccount(account);
};
// Create context value
const contextValue = useMemo<SolanaContextState>(
() => ({
// Static RPC values
rpc,
ws,
chain,
// Dynamic wallet values
wallets,
selectedWallet,
selectedAccount,
isConnected,
setWalletAndAccount
}),
[wallets, selectedWallet, selectedAccount, isConnected]
);
return (
<SolanaContext.Provider value={contextValue}>
{children}
</SolanaContext.Provider>
);
}

Is this page helpful?

Πίνακας Περιεχομένων

Επεξεργασία Σελίδας