Next.js - Solana Kit

Configurez une intégration minimale de portefeuille Solana dans Next.js avec Solana Kit.

Ce guide fournit un exemple minimal d'implémentation de fonctionnalités de portefeuille Solana dans une application Next.js en utilisant @solana/kit. Vous allez créer un bouton de connexion de portefeuille et un composant pour envoyer des transactions.

Application Nextjs KitApplication Nextjs Kit

Pour un exemple plus complet d'utilisation de @solana/kit dans une application React, consultez l'exemple d'application React dans le dépôt Solana Kit.

Ressources

Prérequis

Créer un projet Next.js

Créez un nouveau projet Next.js avec shadcn pour les composants d'interface utilisateur et installez les dépendances Solana requises.

Terminal
$
npx shadcn@latest init

Naviguez vers votre répertoire de projet :

Terminal
$
cd <project-name>

Installer les composants UI

Installez les composants UI shadcn suivants :

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

Installer les dépendances Solana

Installez les dépendances Solana suivantes :

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

Guide d'implémentation

Suivez les étapes ci-dessous et copiez le code fourni dans votre projet.

1. Créer le contexte Solana

Tout d'abord, créez un contexte React qui gère l'état complet du portefeuille pour l'application.

Créez components/solana-provider.tsx et ajoutez le code fourni. Ce composant fournisseur va :

  • Se connecter aux points de terminaison RPC du réseau devnet de Solana
  • Filtrer les portefeuilles Solana disponibles installés dans le navigateur de l'utilisateur
  • Suivre quel portefeuille et quel compte est actuellement connecté
  • Fournir l'état du portefeuille aux composants enfants
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. Mettre à jour le Layout

Ensuite, enveloppez l'application Next.js entière avec le fournisseur Solana.

Mettez à jour app/layout.tsx avec le code fourni. Cette étape :

  • Importe le composant SolanaProvider
  • Enveloppe les composants enfants de l'application avec le SolanaProvider
  • Assure que toutes les pages et composants ont accès aux fonctionnalités du portefeuille
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. Créer le bouton de connexion au portefeuille

Maintenant, construisez le bouton pour connecter et déconnecter les portefeuilles.

Créez components/wallet-connect-button.tsx et ajoutez le code fourni. Ce bouton déroulant :

  • Affiche les portefeuilles disponibles lorsqu'il est cliqué
  • Gère le flux de connexion du portefeuille en utilisant le 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. Créer le composant d'envoi de transaction

Créez un composant qui envoie une transaction invoquant le programme memo pour ajouter un message aux journaux de transaction.

L'objectif de ce composant est de démontrer comment envoyer des transactions avec le portefeuille connecté.

Créez components/memo-card.tsx et ajoutez le code fourni. Ce composant :

  • Permet aux utilisateurs de saisir un message
  • Crée une transaction Solana avec une instruction invoquant le programme memo
  • Demande au portefeuille connecté de signer et d'envoyer la transaction
  • Affiche un lien pour visualiser la transaction sur 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. Mettre à jour la page de l'application

Enfin, mettez à jour la page principale de l'application.

Mettez à jour app/page.tsx avec le code fourni. Cette page :

  • Importe et utilise les composants WalletConnectButton et 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. Exécuter l'application

Maintenant, exécutez l'application pour tester l'intégration du portefeuille.

Terminal
$
npm run dev

Notez que le portefeuille connecté doit être configuré pour se connecter au cluster devnet et approvisionné en SOL devnet pour envoyer des transactions.

1. Créer le contexte Solana

Tout d'abord, créez un contexte React qui gère l'état complet du portefeuille pour l'application.

Créez components/solana-provider.tsx et ajoutez le code fourni. Ce composant fournisseur va :

  • Se connecter aux points de terminaison RPC du réseau devnet de Solana
  • Filtrer les portefeuilles Solana disponibles installés dans le navigateur de l'utilisateur
  • Suivre quel portefeuille et quel compte est actuellement connecté
  • Fournir l'état du portefeuille aux composants enfants

2. Mettre à jour le Layout

Ensuite, enveloppez l'application Next.js entière avec le fournisseur Solana.

Mettez à jour app/layout.tsx avec le code fourni. Cette étape :

  • Importe le composant SolanaProvider
  • Enveloppe les composants enfants de l'application avec le SolanaProvider
  • Assure que toutes les pages et composants ont accès aux fonctionnalités du portefeuille

3. Créer le bouton de connexion au portefeuille

Maintenant, construisez le bouton pour connecter et déconnecter les portefeuilles.

Créez components/wallet-connect-button.tsx et ajoutez le code fourni. Ce bouton déroulant :

  • Affiche les portefeuilles disponibles lorsqu'il est cliqué
  • Gère le flux de connexion du portefeuille en utilisant le Wallet Standard

4. Créer le composant d'envoi de transaction

Créez un composant qui envoie une transaction invoquant le programme memo pour ajouter un message aux journaux de transaction.

L'objectif de ce composant est de démontrer comment envoyer des transactions avec le portefeuille connecté.

Créez components/memo-card.tsx et ajoutez le code fourni. Ce composant :

  • Permet aux utilisateurs de saisir un message
  • Crée une transaction Solana avec une instruction invoquant le programme memo
  • Demande au portefeuille connecté de signer et d'envoyer la transaction
  • Affiche un lien pour visualiser la transaction sur Solana Explorer

5. Mettre à jour la page de l'application

Enfin, mettez à jour la page principale de l'application.

Mettez à jour app/page.tsx avec le code fourni. Cette page :

  • Importe et utilise les composants WalletConnectButton et MemoCard

6. Exécuter l'application

Maintenant, exécutez l'application pour tester l'intégration du portefeuille.

Terminal
$
npm run dev

Notez que le portefeuille connecté doit être configuré pour se connecter au cluster devnet et approvisionné en SOL devnet pour envoyer des transactions.

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?

Table des matières

Modifier la page