Addons

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.json
pnpm dlx shadcn@latest add https://nitso.fun/r/transaction-list-demo.json
yarn dlx shadcn@latest add https://nitso.fun/r/transaction-list-demo.json
bunx --bun shadcn@latest add https://nitso.fun/r/transaction-list-demo.json

This 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.json
pnpm dlx shadcn@latest add https://nitso.fun/r/transaction-list.json
yarn dlx shadcn@latest add https://nitso.fun/r/transaction-list.json
bunx --bun shadcn@latest add https://nitso.fun/r/transaction-list.json

Install shadcn UI components

npx shadcn@latest accordion
pnpm dlx shadcn@latest accordion
yarn dlx shadcn@latest accordion
bunx --bun shadcn@latest accordion

Copy 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:

components/header.tsx
import { ConnectButton } from "@/components/nitso/connect-button/";

export function Header() {
  return (
    <nav>
      <ConnectButton />
    </nav>
  );
}

After:

components/header.tsx
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:

components/header.tsx
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

PropTypeDefaultDescription
limitnumber5Number 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.

On this page