Next.js - Solana Kit

Määritä minimaalinen Solana-lompakon integrointi Next.js:ssä Solana Kitin avulla.

Tämä opas tarjoaa minimaalisen esimerkin Solana-lompakon toiminnallisuuden toteuttamisesta Next.js-sovelluksessa käyttäen @solana/kit. Luot lompakon yhdistämispainikkeen ja komponentin transaktioiden lähettämiseen.

Nextjs Kit -sovellusNextjs Kit -sovellus

Kattavamman esimerkin @solana/kit:n käytöstä React-sovelluksessa löydät React App -esimerkistä Solana Kit -repositoriossa.

Resurssit

Edellytykset

Luo Next.js-projekti

Luo uusi Next.js-projekti shadcn:n avulla käyttöliittymäkomponentteja varten ja asenna tarvittavat Solana-riippuvuudet.

Terminal
$
npx shadcn@latest init

Siirry projektihakemistoosi:

Terminal
$
cd <project-name>

Asenna käyttöliittymäkomponentit

Asenna seuraavat shadcn-käyttöliittymäkomponentit:

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

Asenna Solana-riippuvuudet

Asenna seuraavat Solana-riippuvuudet:

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

Toteutuksen läpikäynti

Seuraa alla olevia vaiheita ja kopioi annettu koodi projektiisi.

1. Luo Solana-konteksti

Luo ensin React-konteksti, joka hallinnoi koko lompakon tilaa sovelluksessa.

Luo components/solana-provider.tsx ja lisää annettu koodi. Tämä tarjoajakomponentti:

  • Yhdistää Solanan devnet RPC -päätepisteisiin
  • Suodattaa käyttäjän selaimeen asennetut saatavilla olevat Solana-lompakot
  • Seuraa, mikä lompakko ja tili on tällä hetkellä yhdistettynä
  • Tarjoaa lompakon tilan lapsikomponenteille
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. Päivitä Layout

Seuraavaksi kääri koko Next.js-sovellus Solana-tarjoajalla.

Päivitä app/layout.tsx annetulla koodilla. Tämä vaihe:

  • Tuo SolanaProvider komponentin
  • Käärii sovelluksen lapsikomponentit SolanaProvider komponentilla
  • Varmistaa, että kaikilla sivuilla ja komponenteilla on pääsy lompakkotoimintoihin
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. Luo lompakon yhdistämispainike

Nyt rakennetaan painike lompakoiden yhdistämiseen ja katkaisemiseen.

Luo components/wallet-connect-button.tsx ja lisää annettu koodi. Tämä pudotusvalikko-painike:

  • Näyttää saatavilla olevat lompakot, kun sitä klikataan
  • Käsittelee lompakon yhdistämisprosessin käyttäen Wallet Standard -protokollaa
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. Luo tapahtuman lähetyskomponentti

Luo komponentti, joka lähettää tapahtuman kutsuen memo-ohjelmaa lisätäkseen viestin käännöslokeihin.

Tämän komponentin tarkoitus on havainnollistaa, miten tapahtumia lähetetään yhdistetyn lompakon avulla.

Luo components/memo-card.tsx ja lisää annettu koodi. Tämä komponentti:

  • Mahdollistaa käyttäjien syöttää viesti
  • Luo Solana-tapahtuman ohjeella, joka kutsuu memo-ohjelmaa
  • Pyytää yhdistettyä lompakkoa allekirjoittamaan ja lähettämään tapahtuman
  • Näyttää linkin tapahtuman tarkasteluun Solana Explorerissa
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. Päivitä sovelluksen sivu

Lopuksi päivitä sovelluksen pääsivu.

Päivitä app/page.tsx annetulla koodilla. Tämä sivu:

  • Tuo ja käyttää WalletConnectButton ja MemoCard komponentteja
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. Suorita sovellus

Suorita nyt sovellus testataksesi lompakon integraatiota.

Terminal
$
npm run dev

Huomaa, että yhdistetty lompakko täytyy määrittää yhdistämään devnet-klusteriin ja rahoittaa devnet SOL:lla tapahtumien lähettämistä varten.

1. Luo Solana-konteksti

Luo ensin React-konteksti, joka hallinnoi koko lompakon tilaa sovelluksessa.

Luo components/solana-provider.tsx ja lisää annettu koodi. Tämä tarjoajakomponentti:

  • Yhdistää Solanan devnet RPC -päätepisteisiin
  • Suodattaa käyttäjän selaimeen asennetut saatavilla olevat Solana-lompakot
  • Seuraa, mikä lompakko ja tili on tällä hetkellä yhdistettynä
  • Tarjoaa lompakon tilan lapsikomponenteille

2. Päivitä Layout

Seuraavaksi kääri koko Next.js-sovellus Solana-tarjoajalla.

Päivitä app/layout.tsx annetulla koodilla. Tämä vaihe:

  • Tuo SolanaProvider komponentin
  • Käärii sovelluksen lapsikomponentit SolanaProvider komponentilla
  • Varmistaa, että kaikilla sivuilla ja komponenteilla on pääsy lompakkotoimintoihin

3. Luo lompakon yhdistämispainike

Nyt rakennetaan painike lompakoiden yhdistämiseen ja katkaisemiseen.

Luo components/wallet-connect-button.tsx ja lisää annettu koodi. Tämä pudotusvalikko-painike:

  • Näyttää saatavilla olevat lompakot, kun sitä klikataan
  • Käsittelee lompakon yhdistämisprosessin käyttäen Wallet Standard -protokollaa

4. Luo tapahtuman lähetyskomponentti

Luo komponentti, joka lähettää tapahtuman kutsuen memo-ohjelmaa lisätäkseen viestin käännöslokeihin.

Tämän komponentin tarkoitus on havainnollistaa, miten tapahtumia lähetetään yhdistetyn lompakon avulla.

Luo components/memo-card.tsx ja lisää annettu koodi. Tämä komponentti:

  • Mahdollistaa käyttäjien syöttää viesti
  • Luo Solana-tapahtuman ohjeella, joka kutsuu memo-ohjelmaa
  • Pyytää yhdistettyä lompakkoa allekirjoittamaan ja lähettämään tapahtuman
  • Näyttää linkin tapahtuman tarkasteluun Solana Explorerissa

5. Päivitä sovelluksen sivu

Lopuksi päivitä sovelluksen pääsivu.

Päivitä app/page.tsx annetulla koodilla. Tämä sivu:

  • Tuo ja käyttää WalletConnectButton ja MemoCard komponentteja

6. Suorita sovellus

Suorita nyt sovellus testataksesi lompakon integraatiota.

Terminal
$
npm run dev

Huomaa, että yhdistetty lompakko täytyy määrittää yhdistämään devnet-klusteriin ja rahoittaa devnet SOL:lla tapahtumien lähettämistä varten.

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?

Sisällysluettelo

Muokkaa sivua