Skip to main content
Skip table of contents

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.

CODE
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

TYPESCRIPT
"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

TYPESCRIPT
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>
  );
}
Base Sepolia Transactions.png

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

CODE
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

TYPESCRIPT
...
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>
  );
}
...

TYPESCRIPT
...
                  <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

Base Sepolia with Callout.png

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

TYPESCRIPT
"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

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.