Transaction List
Lists recent wallet transactions in a collapsible accordion. Requires connect-button.
Preview
"use client";import { formatAddress } from "@solana/connector";import { useTransactions } from "@solana/connector/react";import { ArrowDownLeft, ArrowUpRight, Coins, ExternalLink, History, RefreshCw,} from "lucide-react";import { Accordion, AccordionContent, AccordionItem, AccordionTrigger,} from "@/components/ui/accordion";import { cn } from "@/lib/utils";function getTransactionTitle(tx: { type: string; programName?: string; programId?: string;}) { if (tx.type === "tokenAccountClosed") return "Token Account Closed"; if (tx.type === "program") { const program = tx.programName ?? (tx.programId ? formatAddress(tx.programId) : "Unknown"); return `Program: ${program}`; } return tx.type;}function getTransactionSubtitle(tx: { type: string; formattedTime: string; instructionTypes?: string[];}) { if (tx.type === "program" && tx.instructionTypes?.length) { const summary = tx.instructionTypes.slice(0, 2).join(" · "); return `${tx.formattedTime} · ${summary}`; } return tx.formattedTime;}function SwapTokenIcon({ fromIcon, toIcon, size = 32,}: { fromIcon?: string; toIcon?: string; size?: number;}) { const offset = size * 0.6; return ( <div className="relative shrink-0" style={{ width: size + offset, height: size }} > <div className="bg-muted border-background absolute top-0 left-0 flex items-center justify-center rounded-full border-2" style={{ width: size, height: size }} > {fromIcon ? ( // biome-ignore lint/performance/noImgElement: token logos are dynamic external URLs <img src={fromIcon} className="rounded-full" style={{ width: size - 4, height: size - 4 }} alt="" /> ) : ( <Coins className="text-muted-foreground h-4 w-4" /> )} </div> <div className="bg-muted border-background absolute top-0 flex items-center justify-center rounded-full border-2" style={{ left: offset, width: size, height: size }} > {toIcon ? ( // biome-ignore lint/performance/noImgElement: token logos are dynamic external URLs <img src={toIcon} className="rounded-full" style={{ width: size - 4, height: size - 4 }} alt="" /> ) : ( <Coins className="text-muted-foreground h-4 w-4" /> )} </div> </div> );}interface TransactionListProps { limit?: number;}export function TransactionList({ limit = 5 }: TransactionListProps) { const { transactions, isLoading, refetch } = useTransactions({ limit }); const showSkeleton = isLoading && transactions.length === 0; return ( <Accordion type="single" collapsible className="w-full"> <AccordionItem value="transactions" className="rounded-[12px] border px-3" > <div className="flex items-center justify-between"> <AccordionTrigger className="flex-1 gap-2 py-3 hover:cursor-pointer hover:no-underline"> <div className="flex items-center gap-2"> <History className="h-4 w-4" /> <span className="font-medium">Recent Activity</span> </div> </AccordionTrigger> <button type="button" onClick={(e) => { e.stopPropagation(); refetch(); }} disabled={isLoading} className="hover:bg-accent rounded p-1 transition-colors disabled:opacity-50" > <RefreshCw className={cn("h-3.5 w-3.5", isLoading && "animate-spin")} /> </button> </div> <AccordionContent className="h-auto [&_a]:no-underline [&_p]:mb-0"> {showSkeleton ? ( <div className="space-y-2"> {[1, 2, 3].map((i) => ( <div key={i} className="flex items-center gap-3"> <div className="bg-muted h-8 w-8 animate-pulse rounded-full" /> <div className="flex-1"> <div className="bg-muted mb-1 h-4 w-20 animate-pulse rounded" /> <div className="bg-muted h-3 w-16 animate-pulse rounded" /> </div> </div> ))} </div> ) : transactions.length === 0 ? ( <p className="text-muted-foreground py-2 text-center text-sm"> No transactions yet </p> ) : ( <div className="space-y-2 pb-2"> {transactions.map((tx) => ( <a key={tx.signature} href={tx.explorerUrl} target="_blank" rel="noopener noreferrer" className="hover:bg-muted/50 -mx-1 flex items-center gap-3 rounded-lg px-1 py-1 transition-colors" > <div className="relative shrink-0"> {tx.type === "swap" && (tx.swapFromToken || tx.swapToToken) ? ( <SwapTokenIcon fromIcon={tx.swapFromToken?.icon} toIcon={tx.swapToToken?.icon} size={32} /> ) : tx.tokenIcon ? ( // biome-ignore lint/performance/noImgElement: token logos are dynamic external URLs <img src={tx.tokenIcon} className="h-8 w-8 rounded-full" alt="" /> ) : ( <div className="bg-muted flex h-8 w-8 items-center justify-center rounded-full"> <History className="h-4 w-4" /> </div> )} {(tx.direction === "in" || tx.direction === "out") && ( <div className={cn( "border-background absolute -right-0.5 -bottom-0.5 flex h-4 w-4 items-center justify-center rounded-full border-2", tx.direction === "in" ? "bg-green-500 text-white" : "bg-orange-500 text-white", )} > {tx.direction === "in" ? ( <ArrowDownLeft className="h-2 w-2" /> ) : ( <ArrowUpRight className="h-2 w-2" /> )} </div> )} </div> <div className="flex-1"> <span className="block text-sm font-medium"> {getTransactionTitle(tx)} </span> <span className="text-muted-foreground block text-xs"> {getTransactionSubtitle(tx)} </span> </div> <div className="flex shrink-0 items-center gap-2"> {tx.formattedAmount && ( <span className={cn( "text-sm font-medium tabular-nums", tx.direction === "in" ? "text-green-600" : tx.direction === "out" ? "text-orange-600" : "text-muted-foreground", )} > {tx.formattedAmount} </span> )} <ExternalLink className="text-muted-foreground h-3 w-3" /> </div> </a> ))} </div> )} </AccordionContent> </AccordionItem> </Accordion> );}Prerequisites
Complete the installation guide and install Connect Button before using this addon.
Try the Demo
Want to see it working immediately without manual wiring? Install the demo:
npx shadcn@latest add https://nitso.fun/r/transaction-list-demo.jsonpnpm dlx shadcn@latest add https://nitso.fun/r/transaction-list-demo.jsonyarn dlx shadcn@latest add https://nitso.fun/r/transaction-list-demo.jsonbunx --bun shadcn@latest add https://nitso.fun/r/transaction-list-demo.jsonThis installs transaction-list-demo.tsx into your components/ folder. Drop it anywhere:
import { TransactionListDemo } from '@/components/transaction-list-demo';
export default function Page() {
return <TransactionListDemo />;
}Delete it when you're ready to wire things up yourself.
Installation
npx shadcn@latest add https://nitso.fun/r/transaction-list.jsonpnpm dlx shadcn@latest add https://nitso.fun/r/transaction-list.jsonyarn dlx shadcn@latest add https://nitso.fun/r/transaction-list.jsonbunx --bun shadcn@latest add https://nitso.fun/r/transaction-list.jsonInstall shadcn UI components
npx shadcn@latest accordionpnpm dlx shadcn@latest accordionyarn dlx shadcn@latest accordionbunx --bun shadcn@latest accordionCopy the source code
"use client";import { formatAddress } from "@solana/connector";import { useTransactions } from "@solana/connector/react";import { ArrowDownLeft, ArrowUpRight, Coins, ExternalLink, History, RefreshCw,} from "lucide-react";import { Accordion, AccordionContent, AccordionItem, AccordionTrigger,} from "@/components/ui/accordion";import { cn } from "@/lib/utils";function getTransactionTitle(tx: { type: string; programName?: string; programId?: string;}) { if (tx.type === "tokenAccountClosed") return "Token Account Closed"; if (tx.type === "program") { const program = tx.programName ?? (tx.programId ? formatAddress(tx.programId) : "Unknown"); return `Program: ${program}`; } return tx.type;}function getTransactionSubtitle(tx: { type: string; formattedTime: string; instructionTypes?: string[];}) { if (tx.type === "program" && tx.instructionTypes?.length) { const summary = tx.instructionTypes.slice(0, 2).join(" · "); return `${tx.formattedTime} · ${summary}`; } return tx.formattedTime;}function SwapTokenIcon({ fromIcon, toIcon, size = 32,}: { fromIcon?: string; toIcon?: string; size?: number;}) { const offset = size * 0.6; return ( <div className="relative shrink-0" style={{ width: size + offset, height: size }} > <div className="bg-muted border-background absolute top-0 left-0 flex items-center justify-center rounded-full border-2" style={{ width: size, height: size }} > {fromIcon ? ( // biome-ignore lint/performance/noImgElement: token logos are dynamic external URLs <img src={fromIcon} className="rounded-full" style={{ width: size - 4, height: size - 4 }} alt="" /> ) : ( <Coins className="text-muted-foreground h-4 w-4" /> )} </div> <div className="bg-muted border-background absolute top-0 flex items-center justify-center rounded-full border-2" style={{ left: offset, width: size, height: size }} > {toIcon ? ( // biome-ignore lint/performance/noImgElement: token logos are dynamic external URLs <img src={toIcon} className="rounded-full" style={{ width: size - 4, height: size - 4 }} alt="" /> ) : ( <Coins className="text-muted-foreground h-4 w-4" /> )} </div> </div> );}interface TransactionListProps { limit?: number;}export function TransactionList({ limit = 5 }: TransactionListProps) { const { transactions, isLoading, refetch } = useTransactions({ limit }); const showSkeleton = isLoading && transactions.length === 0; return ( <Accordion type="single" collapsible className="w-full"> <AccordionItem value="transactions" className="rounded-[12px] border px-3" > <div className="flex items-center justify-between"> <AccordionTrigger className="flex-1 gap-2 py-3 hover:cursor-pointer hover:no-underline"> <div className="flex items-center gap-2"> <History className="h-4 w-4" /> <span className="font-medium">Recent Activity</span> </div> </AccordionTrigger> <button type="button" onClick={(e) => { e.stopPropagation(); refetch(); }} disabled={isLoading} className="hover:bg-accent rounded p-1 transition-colors disabled:opacity-50" > <RefreshCw className={cn("h-3.5 w-3.5", isLoading && "animate-spin")} /> </button> </div> <AccordionContent className="h-auto [&_a]:no-underline [&_p]:mb-0"> {showSkeleton ? ( <div className="space-y-2"> {[1, 2, 3].map((i) => ( <div key={i} className="flex items-center gap-3"> <div className="bg-muted h-8 w-8 animate-pulse rounded-full" /> <div className="flex-1"> <div className="bg-muted mb-1 h-4 w-20 animate-pulse rounded" /> <div className="bg-muted h-3 w-16 animate-pulse rounded" /> </div> </div> ))} </div> ) : transactions.length === 0 ? ( <p className="text-muted-foreground py-2 text-center text-sm"> No transactions yet </p> ) : ( <div className="space-y-2 pb-2"> {transactions.map((tx) => ( <a key={tx.signature} href={tx.explorerUrl} target="_blank" rel="noopener noreferrer" className="hover:bg-muted/50 -mx-1 flex items-center gap-3 rounded-lg px-1 py-1 transition-colors" > <div className="relative shrink-0"> {tx.type === "swap" && (tx.swapFromToken || tx.swapToToken) ? ( <SwapTokenIcon fromIcon={tx.swapFromToken?.icon} toIcon={tx.swapToToken?.icon} size={32} /> ) : tx.tokenIcon ? ( // biome-ignore lint/performance/noImgElement: token logos are dynamic external URLs <img src={tx.tokenIcon} className="h-8 w-8 rounded-full" alt="" /> ) : ( <div className="bg-muted flex h-8 w-8 items-center justify-center rounded-full"> <History className="h-4 w-4" /> </div> )} {(tx.direction === "in" || tx.direction === "out") && ( <div className={cn( "border-background absolute -right-0.5 -bottom-0.5 flex h-4 w-4 items-center justify-center rounded-full border-2", tx.direction === "in" ? "bg-green-500 text-white" : "bg-orange-500 text-white", )} > {tx.direction === "in" ? ( <ArrowDownLeft className="h-2 w-2" /> ) : ( <ArrowUpRight className="h-2 w-2" /> )} </div> )} </div> <div className="flex-1"> <span className="block text-sm font-medium"> {getTransactionTitle(tx)} </span> <span className="text-muted-foreground block text-xs"> {getTransactionSubtitle(tx)} </span> </div> <div className="flex shrink-0 items-center gap-2"> {tx.formattedAmount && ( <span className={cn( "text-sm font-medium tabular-nums", tx.direction === "in" ? "text-green-600" : tx.direction === "out" ? "text-orange-600" : "text-muted-foreground", )} > {tx.formattedAmount} </span> )} <ExternalLink className="text-muted-foreground h-3 w-3" /> </div> </a> ))} </div> )} </AccordionContent> </AccordionItem> </Accordion> );}Wiring
Update the file where you use <ConnectButton /> (e.g. your header or layout):
Before:
import { ConnectButton } from "@/components/nitso/connect-button/";
export function Header() {
return (
<nav>
<ConnectButton />
</nav>
);
}After:
import { ConnectButton } from "@/components/nitso/connect-button/";
import {
WalletDropdownContent,
type WalletDropdownContentProps,
} from "@/components/nitso/connect-button/";
import { TransactionList } from "@/components/nitso/addons/transaction-list/";
function WalletDropdownWithTransactions(props: WalletDropdownContentProps) {
return (
<WalletDropdownContent {...props}>
<TransactionList /> // [!code ++]
</WalletDropdownContent>
);
}
export function Header() {
return (
<nav>
<ConnectButton /> // [!code --]
<ConnectButton dropdownContent={WalletDropdownWithTransactions} /> //
[!code ++]
</nav>
);
}TransactionList plugs into the children slot of WalletDropdownContent, rendering a collapsible accordion with recent activity between the wallet header and the disconnect button.
Combining with other addons
If you already have other addons wired up, add TransactionList as another child:
import { useState } from "react";
import { ConnectButton } from "@/components/nitso/connect-button/";
import {
WalletDropdownContent,
type WalletDropdownContentProps,
} from "@/components/nitso/connect-button/";
import {
NetworkSwitcherButton,
NetworkSwitcherView,
} from "@/components/nitso/addons/network-switcher/";
import { WalletBalance } from "@/components/nitso/addons/wallet-balance/";
import { TokenList } from "@/components/nitso/addons/token-list/";
import { TransactionList } from "@/components/nitso/addons/transaction-list/";
function WalletDropdownFull(props: WalletDropdownContentProps) {
const [view, setView] = useState<"wallet" | "network">("wallet");
if (view === "network") {
return <NetworkSwitcherView onBack={() => setView("wallet")} />;
}
return (
<WalletDropdownContent
{...props}
actions={<NetworkSwitcherButton onClick={() => setView("network")} />}
>
<WalletBalance />
<TokenList />
<TransactionList /> // [!code ++]
</WalletDropdownContent>
);
}
export function Header() {
return (
<nav>
<ConnectButton dropdownContent={WalletDropdownFull} />
</nav>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
limit | number | 5 | Number of recent transactions to display. |
Notes
Transaction history is fetched from your configured RPC endpoint. Each transaction links to Solana Explorer for full details. Transaction types (sent, received, swap, stake, program interaction) are automatically detected and displayed with appropriate icons.
To show human-readable program names instead of raw addresses, configure programLabels in your NitsoProvider.