Next.js - Solana Kit

Siapkan integrasi dompet Solana minimal di Next.js dengan Solana Kit.

Panduan ini memberikan contoh minimal untuk mengimplementasikan fungsionalitas dompet Solana dalam aplikasi Next.js menggunakan @solana/kit. Anda akan membuat tombol koneksi dompet dan komponen untuk mengirim transaksi.

Aplikasi Nextjs KitAplikasi Nextjs Kit

Untuk contoh yang lebih komprehensif tentang penggunaan @solana/kit dalam aplikasi React, lihat Contoh Aplikasi React di repositori Solana Kit.

Sumber Daya

Prasyarat

Membuat Proyek Next.js

Buat proyek Next.js baru dengan shadcn untuk komponen UI dan instal dependensi Solana yang diperlukan.

Terminal
$
npx shadcn@latest init

Navigasi ke direktori proyek Anda:

Terminal
$
cd <project-name>

Instal Komponen UI

Instal komponen UI shadcn berikut:

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

Instal Dependensi Solana

Instal dependensi Solana berikut:

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

Panduan Implementasi

Ikuti langkah-langkah di bawah ini dan salin kode yang disediakan ke proyek Anda.

1. Membuat Solana Context

Pertama, buat React Context yang mengelola seluruh status dompet untuk aplikasi.

Buat components/solana-provider.tsx dan tambahkan kode yang disediakan. Komponen provider ini akan:

  • Terhubung ke endpoint RPC devnet Solana
  • Memfilter dompet Solana yang tersedia yang terpasang di browser pengguna
  • Melacak dompet dan akun mana yang saat ini terhubung
  • Menyediakan status dompet ke komponen turunan
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. Memperbarui Layout

Selanjutnya, bungkus seluruh aplikasi Next.js dengan provider Solana.

Perbarui app/layout.tsx dengan kode yang disediakan. Langkah ini:

  • Mengimpor komponen SolanaProvider
  • Membungkus komponen turunan aplikasi dengan SolanaProvider
  • Memastikan semua halaman dan komponen memiliki akses ke fungsionalitas dompet
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. Membuat Tombol Koneksi Dompet

Sekarang buat tombol untuk menghubungkan dan memutuskan dompet.

Buat components/wallet-connect-button.tsx dan tambahkan kode yang disediakan. Tombol dropdown ini:

  • Menampilkan dompet yang tersedia saat diklik
  • Menangani alur koneksi dompet menggunakan 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. Membuat Komponen Kirim Transaksi

Buat komponen yang mengirim transaksi dengan memanggil program memo untuk menambahkan pesan ke log transaksi.

Tujuan dari komponen ini adalah untuk mendemonstrasikan cara mengirim transaksi dengan dompet yang terhubung.

Buat components/memo-card.tsx dan tambahkan kode yang disediakan. Komponen ini:

  • Memungkinkan pengguna memasukkan pesan
  • Membuat transaksi Solana dengan instruksi yang memanggil program memo
  • Meminta dompet yang terhubung untuk menandatangani dan mengirim transaksi
  • Menampilkan tautan untuk melihat transaksi di 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. Perbarui Halaman App

Terakhir, perbarui halaman utama aplikasi.

Perbarui app/page.tsx dengan kode yang disediakan. Halaman ini:

  • Mengimpor dan menggunakan komponen WalletConnectButton dan 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. Jalankan Aplikasi

Sekarang jalankan aplikasi untuk menguji integrasi dompet.

Terminal
$
npm run dev

Perhatikan bahwa dompet yang terhubung harus dikonfigurasi untuk terhubung ke cluster devnet dan diisi dengan devnet SOL untuk mengirim transaksi.

1. Membuat Solana Context

Pertama, buat React Context yang mengelola seluruh status dompet untuk aplikasi.

Buat components/solana-provider.tsx dan tambahkan kode yang disediakan. Komponen provider ini akan:

  • Terhubung ke endpoint RPC devnet Solana
  • Memfilter dompet Solana yang tersedia yang terpasang di browser pengguna
  • Melacak dompet dan akun mana yang saat ini terhubung
  • Menyediakan status dompet ke komponen turunan

2. Memperbarui Layout

Selanjutnya, bungkus seluruh aplikasi Next.js dengan provider Solana.

Perbarui app/layout.tsx dengan kode yang disediakan. Langkah ini:

  • Mengimpor komponen SolanaProvider
  • Membungkus komponen turunan aplikasi dengan SolanaProvider
  • Memastikan semua halaman dan komponen memiliki akses ke fungsionalitas dompet

3. Membuat Tombol Koneksi Dompet

Sekarang buat tombol untuk menghubungkan dan memutuskan dompet.

Buat components/wallet-connect-button.tsx dan tambahkan kode yang disediakan. Tombol dropdown ini:

  • Menampilkan dompet yang tersedia saat diklik
  • Menangani alur koneksi dompet menggunakan Wallet Standard

4. Membuat Komponen Kirim Transaksi

Buat komponen yang mengirim transaksi dengan memanggil program memo untuk menambahkan pesan ke log transaksi.

Tujuan dari komponen ini adalah untuk mendemonstrasikan cara mengirim transaksi dengan dompet yang terhubung.

Buat components/memo-card.tsx dan tambahkan kode yang disediakan. Komponen ini:

  • Memungkinkan pengguna memasukkan pesan
  • Membuat transaksi Solana dengan instruksi yang memanggil program memo
  • Meminta dompet yang terhubung untuk menandatangani dan mengirim transaksi
  • Menampilkan tautan untuk melihat transaksi di Solana Explorer

5. Perbarui Halaman App

Terakhir, perbarui halaman utama aplikasi.

Perbarui app/page.tsx dengan kode yang disediakan. Halaman ini:

  • Mengimpor dan menggunakan komponen WalletConnectButton dan MemoCard

6. Jalankan Aplikasi

Sekarang jalankan aplikasi untuk menguji integrasi dompet.

Terminal
$
npm run dev

Perhatikan bahwa dompet yang terhubung harus dikonfigurasi untuk terhubung ke cluster devnet dan diisi dengan devnet SOL untuk mengirim transaksi.

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?

Daftar Isi

Edit Halaman