Next.js - Solana Kit

Solana Kit ile Next.js'de minimal bir Solana cüzdan entegrasyonu kurun.

Bu rehber, @solana/kit kullanarak bir Next.js uygulamasında Solana cüzdan işlevselliğini uygulamanın minimal bir örneğini sunmaktadır. Bir cüzdan bağlantı butonu ve işlem göndermek için bir bileşen oluşturacaksınız.

Nextjs Kit UygulamasıNextjs Kit Uygulaması

Bir React uygulamasında @solana/kit kullanımının daha kapsamlı bir örneği için, Solana Kit deposundaki React Uygulama Örneği'ne bakın.

Kaynaklar

Ön Koşullar

Next.js Projesi Oluşturma

UI bileşenleri için shadcn ile yeni bir Next.js projesi oluşturun ve gerekli Solana bağımlılıklarını yükleyin.

Terminal
$
npx shadcn@latest init

Proje dizininize gidin:

Terminal
$
cd <project-name>

UI Bileşenlerini Yükleme

Aşağıdaki shadcn ui bileşenlerini yükleyin:

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

Solana Bağımlılıklarını Yükleme

Aşağıdaki Solana bağımlılıklarını yükleyin:

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

Uygulama Adımları

Aşağıdaki adımları takip edin ve sağlanan kodları projenize kopyalayın.

1. Solana Context Oluşturma

İlk olarak, uygulama için tüm cüzdan durumunu yöneten bir React Context oluşturun.

components/solana-provider.tsx dosyasını oluşturun ve sağlanan kodu ekleyin. Bu sağlayıcı bileşen şunları yapacaktır:

  • Solana'nın devnet RPC uç noktalarına bağlanma
  • Kullanıcının tarayıcısında yüklü olan mevcut Solana cüzdanlarını filtreleme
  • Hangi cüzdanın ve hesabın şu anda bağlı olduğunu takip etme
  • Alt bileşenlere cüzdan durumunu sağlama
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'u Güncelleme

Sonraki adımda, tüm Next.js uygulamasını Solana sağlayıcısı ile sarmalayın.

app/layout.tsx dosyasını sağlanan kodla güncelleyin. Bu adım:

  • SolanaProvider bileşenini içe aktarır
  • Uygulamanın alt bileşenlerini SolanaProvider ile sarar
  • Tüm sayfaların ve bileşenlerin cüzdan işlevselliğine erişimini sağlar
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. Cüzdan Bağlantı Butonu Oluşturma

Şimdi cüzdanları bağlamak ve bağlantıyı kesmek için bir buton oluşturun.

components/wallet-connect-button.tsx dosyasını oluşturun ve sağlanan kodu ekleyin. Bu açılır menü butonu:

  • Tıklandığında mevcut cüzdanları gösterir
  • Cüzdan Standardını kullanarak cüzdan bağlantı akışını yönetir
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. İşlem Gönderme Bileşeni Oluşturma

Memo programını çağırarak işlem kaydı günlüklerine mesaj ekleyen bir bileşen oluşturun.

Bu bileşenin amacı, bağlı cüzdan ile nasıl işlem gönderileceğini göstermektir.

components/memo-card.tsx oluşturun ve sağlanan kodu ekleyin. Bu bileşen:

  • Kullanıcıların bir mesaj girmesine olanak tanır
  • Memo programını çağıran bir talimatla bir Solana işlemi oluşturur
  • Bağlı cüzdandan işlemi imzalamasını ve göndermesini ister
  • İşlemi Solana Explorer'da görüntülemek için bir bağlantı gösterir
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. Uygulama Sayfasını Güncelleme

Son olarak, ana uygulama sayfasını güncelleyin.

app/page.tsx dosyasını sağlanan kodla güncelleyin. Bu sayfa:

  • WalletConnectButton ve MemoCard bileşenlerini içe aktarır ve kullanır
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. Uygulamayı Çalıştırma

Şimdi cüzdan entegrasyonunu test etmek için uygulamayı çalıştırın.

Terminal
$
npm run dev

Bağlı cüzdanın devnet kümesine bağlanacak şekilde yapılandırılması ve işlem göndermek için devnet SOL ile fonlanması gerektiğini unutmayın.

1. Solana Context Oluşturma

İlk olarak, uygulama için tüm cüzdan durumunu yöneten bir React Context oluşturun.

components/solana-provider.tsx dosyasını oluşturun ve sağlanan kodu ekleyin. Bu sağlayıcı bileşen şunları yapacaktır:

  • Solana'nın devnet RPC uç noktalarına bağlanma
  • Kullanıcının tarayıcısında yüklü olan mevcut Solana cüzdanlarını filtreleme
  • Hangi cüzdanın ve hesabın şu anda bağlı olduğunu takip etme
  • Alt bileşenlere cüzdan durumunu sağlama

2. Layout'u Güncelleme

Sonraki adımda, tüm Next.js uygulamasını Solana sağlayıcısı ile sarmalayın.

app/layout.tsx dosyasını sağlanan kodla güncelleyin. Bu adım:

  • SolanaProvider bileşenini içe aktarır
  • Uygulamanın alt bileşenlerini SolanaProvider ile sarar
  • Tüm sayfaların ve bileşenlerin cüzdan işlevselliğine erişimini sağlar

3. Cüzdan Bağlantı Butonu Oluşturma

Şimdi cüzdanları bağlamak ve bağlantıyı kesmek için bir buton oluşturun.

components/wallet-connect-button.tsx dosyasını oluşturun ve sağlanan kodu ekleyin. Bu açılır menü butonu:

  • Tıklandığında mevcut cüzdanları gösterir
  • Cüzdan Standardını kullanarak cüzdan bağlantı akışını yönetir

4. İşlem Gönderme Bileşeni Oluşturma

Memo programını çağırarak işlem kaydı günlüklerine mesaj ekleyen bir bileşen oluşturun.

Bu bileşenin amacı, bağlı cüzdan ile nasıl işlem gönderileceğini göstermektir.

components/memo-card.tsx oluşturun ve sağlanan kodu ekleyin. Bu bileşen:

  • Kullanıcıların bir mesaj girmesine olanak tanır
  • Memo programını çağıran bir talimatla bir Solana işlemi oluşturur
  • Bağlı cüzdandan işlemi imzalamasını ve göndermesini ister
  • İşlemi Solana Explorer'da görüntülemek için bir bağlantı gösterir

5. Uygulama Sayfasını Güncelleme

Son olarak, ana uygulama sayfasını güncelleyin.

app/page.tsx dosyasını sağlanan kodla güncelleyin. Bu sayfa:

  • WalletConnectButton ve MemoCard bileşenlerini içe aktarır ve kullanır

6. Uygulamayı Çalıştırma

Şimdi cüzdan entegrasyonunu test etmek için uygulamayı çalıştırın.

Terminal
$
npm run dev

Bağlı cüzdanın devnet kümesine bağlanacak şekilde yapılandırılması ve işlem göndermek için devnet SOL ile fonlanması gerektiğini unutmayın.

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?

İçindekiler

Sayfayı Düzenle