وثائق سولاناالواجهة الأمامية

Next.js - سولانا كيت

إعداد تكامل محفظة سولانا بشكل مبسط في Next.js مع سولانا كيت.

يقدم هذا الدليل مثالاً مبسطاً لتنفيذ وظائف محفظة سولانا في تطبيق Next.js باستخدام @solana/kit. ستقوم بإنشاء زر اتصال بالمحفظة ومكون لإرسال المعاملات.

تطبيق Nextjs Kitتطبيق Nextjs Kit

للحصول على مثال أكثر شمولاً لاستخدام @solana/kit في تطبيق React، يرجى الرجوع إلى مثال تطبيق React في مستودع سولانا كيت.

الموارد

المتطلبات الأساسية

إنشاء مشروع Next.js

قم بإنشاء مشروع Next.js جديد مع shadcn لمكونات واجهة المستخدم وتثبيت تبعيات سولانا المطلوبة.

Terminal
$
npx shadcn@latest init

انتقل إلى دليل المشروع الخاص بك:

Terminal
$
cd <project-name>

تثبيت مكونات واجهة المستخدم

قم بتثبيت مكونات واجهة المستخدم shadcn التالية:

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

تثبيت تبعيات سولانا

قم بتثبيت تبعيات سولانا التالية:

Terminal
$
npm install @solana/kit @solana/react @wallet-standard/core @wallet-standard/react @solana-program/memo

شرح التنفيذ

اتبع الخطوات أدناه وانسخ الكود المقدم إلى مشروعك.

1. إنشاء سياق سولانا

أولاً، قم بإنشاء سياق React الذي يدير حالة المحفظة بالكامل للتطبيق.

قم بإنشاء components/solana-provider.tsx وأضف الكود المقدم. سيقوم مكون المزود هذا بما يلي:

  • الاتصال بنقاط نهاية RPC لشبكة سولانا التجريبية (devnet)
  • تصفية محافظ سولانا المتاحة المثبتة في متصفح المستخدم
  • تتبع المحفظة والحساب المتصل حالياً
  • توفير حالة المحفظة للمكونات الفرعية
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. تحديث التخطيط

بعد ذلك، قم بتغليف تطبيق Next.js بالكامل بمزود سولانا.

قم بتحديث app/layout.tsx بالكود المقدم. تتضمن هذه الخطوة:

  • استيراد مكون SolanaProvider
  • تغليف المكونات الفرعية للتطبيق بـ SolanaProvider
  • ضمان وصول جميع الصفحات والمكونات إلى وظائف المحفظة
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. إنشاء مكون إرسال المعاملات

قم بإنشاء مكون يرسل معاملة تستدعي برنامج المذكرة لإضافة رسالة إلى سجلات الترجمة.

الغرض من هذا المكون هو توضيح كيفية إرسال المعاملات باستخدام المحفظة المتصلة.

قم بإنشاء components/memo-card.tsx وأضف الكود المقدم. هذا المكون:

  • يسمح للمستخدمين بإدخال رسالة
  • ينشئ معاملة سولانا مع تعليمات تستدعي برنامج المذكرة
  • يطلب من المحفظة المتصلة التوقيع وإرسال المعاملة
  • يعرض رابطًا لعرض المعاملة على مستكشف سولانا
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/page.tsx بالكود المقدم. هذه الصفحة:

  • تستورد وتستخدم المكونات 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

لاحظ أنه يجب تكوين المحفظة المتصلة للاتصال بشبكة devnet وأن تكون ممولة بعملة SOL من devnet لإرسال المعاملات.

1. إنشاء سياق سولانا

أولاً، قم بإنشاء سياق React الذي يدير حالة المحفظة بالكامل للتطبيق.

قم بإنشاء components/solana-provider.tsx وأضف الكود المقدم. سيقوم مكون المزود هذا بما يلي:

  • الاتصال بنقاط نهاية RPC لشبكة سولانا التجريبية (devnet)
  • تصفية محافظ سولانا المتاحة المثبتة في متصفح المستخدم
  • تتبع المحفظة والحساب المتصل حالياً
  • توفير حالة المحفظة للمكونات الفرعية

2. تحديث التخطيط

بعد ذلك، قم بتغليف تطبيق Next.js بالكامل بمزود سولانا.

قم بتحديث app/layout.tsx بالكود المقدم. تتضمن هذه الخطوة:

  • استيراد مكون SolanaProvider
  • تغليف المكونات الفرعية للتطبيق بـ SolanaProvider
  • ضمان وصول جميع الصفحات والمكونات إلى وظائف المحفظة

3. إنشاء زر اتصال المحفظة

الآن قم ببناء الزر لتوصيل وفصل المحافظ.

قم بإنشاء components/wallet-connect-button.tsx وأضف الكود المقدم. زر القائمة المنسدلة هذا:

  • يعرض المحافظ المتاحة عند النقر عليه
  • يتعامل مع تدفق اتصال المحفظة باستخدام معيار المحفظة (Wallet Standard)

4. إنشاء مكون إرسال المعاملات

قم بإنشاء مكون يرسل معاملة تستدعي برنامج المذكرة لإضافة رسالة إلى سجلات الترجمة.

الغرض من هذا المكون هو توضيح كيفية إرسال المعاملات باستخدام المحفظة المتصلة.

قم بإنشاء components/memo-card.tsx وأضف الكود المقدم. هذا المكون:

  • يسمح للمستخدمين بإدخال رسالة
  • ينشئ معاملة سولانا مع تعليمات تستدعي برنامج المذكرة
  • يطلب من المحفظة المتصلة التوقيع وإرسال المعاملة
  • يعرض رابطًا لعرض المعاملة على مستكشف سولانا

5. تحديث صفحة التطبيق

أخيرًا، قم بتحديث صفحة التطبيق الرئيسية.

قم بتحديث app/page.tsx بالكود المقدم. هذه الصفحة:

  • تستورد وتستخدم المكونات WalletConnectButton و MemoCard

6. تشغيل التطبيق

الآن قم بتشغيل التطبيق لاختبار تكامل المحفظة.

Terminal
$
npm run dev

لاحظ أنه يجب تكوين المحفظة المتصلة للاتصال بشبكة devnet وأن تكون ممولة بعملة SOL من devnet لإرسال المعاملات.

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?

جدول المحتويات

تعديل الصفحة