5. Show Transaction History
Now, we’ll use the queries prepared in 4. Prepare your Data Queries to show past transactions in your dApp
1. Show most recent transactions for all users
Before users connect their wallet they will still be able to see transactions for all users.
1.1) Configure GraphQL Query Endpoint
Open your SubQuery project in app.onfinality.io and copy the Query URL

Add a entry into your Environment file, e.g.
NEXT_PUBLIC_ONFINALITY_RPC_URL=https://base-sepolia.api.onfinality.io/rpc?apikey=YOUR_API_KEY
NEXT_PUBLIC_SUBQUERY_URL=https://index-api.onfinality.io/sq/YOUR_WORKSPACE_ID/base-sepolia-transactions
1.2) Create Component TransactionHistory
We will create a new component …src/components/TransactionHistory to display historical transactions
Because blockchain IDs are quite large, we want to only show the masked addresses with a button to copy the complete address.
Notice in line 56 that we use the query created in 4. Prepare your Data Queries
"use client";
import { useEffect, useState } from "react";
const SUBQUERY_URL = process.env.NEXT_PUBLIC_SUBQUERY_URL!;
interface Transaction {
id: string;
blockHeight: string;
from: string;
to: string | null;
value: string;
timestamp: string;
}
function CopyButton({ value }: { value: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
await navigator.clipboard.writeText(value);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
return (
<button
onClick={handleCopy}
className="ml-2 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 transition-colors"
title="Copy to clipboard"
>
{copied ? (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
)}
</button>
);
}
export function TransactionHistory() {
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchTransactions() {
try {
const response = await fetch(SUBQUERY_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `{
transactions(first: 10, orderBy: BLOCK_HEIGHT_DESC) {
nodes {
id
blockHeight
from
to
value
timestamp
}
}
}`,
}),
});
const data = await response.json();
if (data.errors) {
throw new Error(data.errors[0].message);
}
setTransactions(data.data.transactions.nodes);
} catch (err) {
console.error("Failed to fetch transactions:", err);
setError(err instanceof Error ? err.message : "Failed to fetch");
} finally {
setLoading(false);
}
}
fetchTransactions();
}, []);
const formatAddress = (address: string | null) => {
if (!address) return "—";
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};
const formatAmount = (wei: string) => {
const eth = Number(BigInt(wei)) / 1e18;
return eth.toFixed(6);
};
const formatTime = (timestamp: string) => {
const date = new Date(Number(timestamp) * 1000);
return date.toLocaleString();
};
return (
<div className="w-full max-w-6xl mx-auto p-4">
<h2 className="text-2xl font-bold mb-4 text-zinc-900 dark:text-zinc-100">
Base Sepolia Transactions
</h2>
{loading && (
<p className="text-zinc-600 dark:text-zinc-400">Loading transactions...</p>
)}
{error && (
<p className="text-red-500">Error: {error}</p>
)}
{!loading && !error && (
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b border-zinc-200 dark:border-zinc-700">
<th className="text-left p-3 font-semibold text-zinc-900 dark:text-zinc-100">Transaction ID</th>
<th className="text-left p-3 font-semibold text-zinc-900 dark:text-zinc-100">Block</th>
<th className="text-left p-3 font-semibold text-zinc-900 dark:text-zinc-100">From</th>
<th className="text-left p-3 font-semibold text-zinc-900 dark:text-zinc-100">To</th>
<th className="text-right p-3 font-semibold text-zinc-900 dark:text-zinc-100">Amount (ETH)</th>
<th className="text-right p-3 font-semibold text-zinc-900 dark:text-zinc-100">Time</th>
</tr>
</thead>
<tbody>
{transactions.map((tx) => (
<tr key={tx.id} className="border-b border-zinc-100 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-900">
<td className="p-3 font-mono text-zinc-700 dark:text-zinc-300">
<span className="inline-flex items-center">
{formatAddress(tx.id)}
<CopyButton value={tx.id} />
</span>
</td>
<td className="p-3 text-zinc-700 dark:text-zinc-300">{tx.blockHeight}</td>
<td className="p-3 font-mono text-zinc-700 dark:text-zinc-300">
<span className="inline-flex items-center">
{formatAddress(tx.from)}
<CopyButton value={tx.from} />
</span>
</td>
<td className="p-3 font-mono text-zinc-700 dark:text-zinc-300">
{tx.to ? (
<span className="inline-flex items-center">
{formatAddress(tx.to)}
<CopyButton value={tx.to} />
</span>
) : (
"—"
)}
</td>
<td className="p-3 text-right text-zinc-700 dark:text-zinc-300">{formatAmount(tx.value)}</td>
<td className="p-3 text-right text-zinc-600 dark:text-zinc-400">{formatTime(tx.timestamp)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{!loading && !error && transactions.length === 0 && (
<p className="text-zinc-600 dark:text-zinc-400">No transactions found.</p>
)}
</div>
);
}
1.3) Add to page.tsx
Update …src/app.page.tsx to include the new table
import { ConnectButton } from "@/components/ConnectButton";
import { WalletBalance } from "@/components/WalletBalance";
import { TransactionHistory } from "@/components/TransactionHistory";
export default function Home() {
return (
<main className="min-h-screen bg-zinc-50 dark:bg-black">
<header className="flex items-center justify-end gap-4 p-4">
<WalletBalance />
<ConnectButton />
</header>
<div className="flex justify-center py-8">
<TransactionHistory />
</div>
</main>
);
}

Because we’re currently showing transactions for all wallets, it will still be shown when a wallet isn’t connected.
2) Add Callout to Explorer
It’s a nice touch to add a callout to open a transaction in the Base Sepolia Explorer
The format is https://sepolia.basescan.org/tx/TRANSACTION_ID
Let’s add https://sepolia.basescan.org/tx/ into the Environment file, allowing it to be easily swapped out for other explorers or networks
NEXT_PUBLIC_ONFINALITY_RPC_URL=https://base-sepolia.api.onfinality.io/rpc?apikey=e411335f-e47a-4e7b-8c2f-fcc7913478cc
NEXT_PUBLIC_SUBQUERY_URL=https://index-api.onfinality.io/sq/6855716396769845248/base-sepolia-transactions
NEXT_PUBLIC_EXPLORER_TX_URL=https://sepolia.basescan.org/tx/
Now we add the callout in TransactionHistory.tsx
...
function ExplorerLink({ txHash }: { txHash: string }) {
return (
<a
href={`${EXPLORER_TX_URL}${txHash}`}
target="_blank"
rel="noopener noreferrer"
className="ml-2 text-zinc-400 hover:text-blue-500 transition-colors"
title="View in explorer"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
);
}
...
...
<td className="p-3 font-mono text-zinc-700 dark:text-zinc-300">
<span className="inline-flex items-center">
{formatAddress(tx.id)}
<CopyButton value={tx.id} />
<ExplorerLink txHash={tx.id} />
</span>
</td>
...
Result

3. Filter to Connected Wallet’s Transactions
Once the user connects their wallet they may want to see only their own transactions.
In this step, we’re going to:
Add a filter tab between All vs My transactions
Make My Transactions only available when a wallet is connected
Default to My Transactions when the wallet connects
Filter the Transactions Query when My Transactions is selected
Update TransactionHistory.tsx
"use client";
import { useEffect, useState } from "react";
import { useAccount } from "wagmi";
const SUBQUERY_URL = process.env.NEXT_PUBLIC_SUBQUERY_URL!;
const EXPLORER_TX_URL = process.env.NEXT_PUBLIC_EXPLORER_TX_URL!;
interface Transaction {
id: string;
blockHeight: string;
from: string;
to: string | null;
value: string;
timestamp: string;
}
type FilterType = "all" | "my";
function ExplorerLink({ txHash }: { txHash: string }) {
return (
<a
href={`${EXPLORER_TX_URL}${txHash}`}
target="_blank"
rel="noopener noreferrer"
className="ml-2 text-zinc-400 hover:text-blue-500 transition-colors"
title="View in explorer"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
);
}
function CopyButton({ value }: { value: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
await navigator.clipboard.writeText(value);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
return (
<button
onClick={handleCopy}
className="ml-2 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 transition-colors"
title="Copy to clipboard"
>
{copied ? (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
)}
</button>
);
}
export function TransactionHistory() {
const { address, isConnected } = useAccount();
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [filter, setFilter] = useState<FilterType>("all");
const fetchTransactions = async (filterToUse: FilterType, walletAddress?: string) => {
setLoading(true);
setError(null);
const query = filterToUse === "my" && walletAddress
? `{
transactions(
first: 10
orderBy: BLOCK_HEIGHT_DESC
filter: {
or: [
{ from: { equalToInsensitive: "${walletAddress}" } }
{ to: { equalToInsensitive: "${walletAddress}" } }
]
}
) {
nodes {
id
blockHeight
from
to
value
timestamp
}
}
}`
: `{
transactions(first: 10, orderBy: BLOCK_HEIGHT_DESC) {
nodes {
id
blockHeight
from
to
value
timestamp
}
}
}`;
try {
const response = await fetch(SUBQUERY_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query }),
});
const data = await response.json();
if (data.errors) {
throw new Error(data.errors[0].message);
}
setTransactions(data.data.transactions.nodes);
} catch (err) {
console.error("Failed to fetch transactions:", err);
setError(err instanceof Error ? err.message : "Failed to fetch");
} finally {
setLoading(false);
}
};
useEffect(() => {
const newFilter = isConnected && address ? "my" : "all";
setFilter(newFilter);
fetchTransactions(newFilter, address);
}, [isConnected, address]);
const handleFilterChange = (newFilter: FilterType) => {
setFilter(newFilter);
fetchTransactions(newFilter, address);
};
const formatAddress = (address: string | null) => {
if (!address) return "—";
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};
const formatAmount = (wei: string) => {
const eth = Number(BigInt(wei)) / 1e18;
return eth.toFixed(6);
};
const formatTime = (timestamp: string) => {
const date = new Date(Number(timestamp) * 1000);
return date.toLocaleString();
};
return (
<div className="w-full max-w-6xl mx-auto p-4">
<h2 className="text-2xl font-bold mb-4 text-zinc-900 dark:text-zinc-100">
Base Sepolia Transactions
</h2>
{isConnected && (
<div className="flex gap-2 mb-4">
<button
onClick={() => handleFilterChange("all")}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
filter === "all"
? "bg-foreground text-background"
: "bg-zinc-100 text-zinc-700 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
}`}
>
All Transactions
</button>
<button
onClick={() => handleFilterChange("my")}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
filter === "my"
? "bg-foreground text-background"
: "bg-zinc-100 text-zinc-700 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
}`}
>
My Transactions
</button>
</div>
)}
{loading && (
<p className="text-zinc-600 dark:text-zinc-400">Loading transactions...</p>
)}
{error && (
<p className="text-red-500">Error: {error}</p>
)}
{!loading && !error && (
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b border-zinc-200 dark:border-zinc-700">
<th className="text-left p-3 font-semibold text-zinc-900 dark:text-zinc-100">Transaction ID</th>
<th className="text-left p-3 font-semibold text-zinc-900 dark:text-zinc-100">Block</th>
<th className="text-left p-3 font-semibold text-zinc-900 dark:text-zinc-100">From</th>
<th className="text-left p-3 font-semibold text-zinc-900 dark:text-zinc-100">To</th>
<th className="text-right p-3 font-semibold text-zinc-900 dark:text-zinc-100">Amount (ETH)</th>
<th className="text-right p-3 font-semibold text-zinc-900 dark:text-zinc-100">Time</th>
</tr>
</thead>
<tbody>
{transactions.map((tx) => (
<tr key={tx.id} className="border-b border-zinc-100 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-900">
<td className="p-3 font-mono text-zinc-700 dark:text-zinc-300">
<span className="inline-flex items-center">
{formatAddress(tx.id)}
<CopyButton value={tx.id} />
<ExplorerLink txHash={tx.id} />
</span>
</td>
<td className="p-3 text-zinc-700 dark:text-zinc-300">{tx.blockHeight}</td>
<td className="p-3 font-mono text-zinc-700 dark:text-zinc-300">
<span className="inline-flex items-center">
{formatAddress(tx.from)}
<CopyButton value={tx.from} />
</span>
</td>
<td className="p-3 font-mono text-zinc-700 dark:text-zinc-300">
{tx.to ? (
<span className="inline-flex items-center">
{formatAddress(tx.to)}
<CopyButton value={tx.to} />
</span>
) : (
"—"
)}
</td>
<td className="p-3 text-right text-zinc-700 dark:text-zinc-300">{formatAmount(tx.value)}</td>
<td className="p-3 text-right text-zinc-600 dark:text-zinc-400">{formatTime(tx.timestamp)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{!loading && !error && transactions.length === 0 && (
<p className="text-zinc-600 dark:text-zinc-400">No transactions found.</p>
)}
</div>
);
}

Beautiful. You should already see a single transaction from when you requested Base Sepolia Testnet ETH in the faucet in 3. Get Wallet Balance via RPC
Next: 6. Send Transfer