Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { DASHBOARD_THIRDWEB_CLIENT_ID } from "@/constants/env";

export interface Transaction {
chain_id: number;
hash: string;
nonce: number;
block_hash: string;
block_number: number;
block_timestamp: number;
transaction_index: number;
from_address: string;
to_address: string | null;
value: number;
gas: number;
gas_price: number | null;
data: string | null;
function_selector: string | null;
max_fee_per_gas: number | null;
max_priority_fee_per_gas: number | null;
transaction_type: number | null;
r: string | null;
s: string | null;
v: number | null;
access_list_json: string | null;
contract_address: string | null;
gas_used: number | null;
cumulative_gas_used: number | null;
effective_gas_price: number | null;
blob_gas_used: number | null;
blob_gas_price: number | null;
logs_bloom: string | null;
status: boolean | null; // true for success, false for failure
}

interface InsightsResponse {
meta: {
address: string;
signature: string;
page: number;
total_items: number;
total_pages: number;
limit_per_chain: number;
chain_ids: number[];
};
data: Transaction[];
}

export async function fetchTxActivity(args: {
chainId: number;
address: string;
limit_per_type?: number,
page?: number;
}): Promise<Transaction[]> {
let { chainId, address, limit_per_type, page } = args;
if (!limit_per_type) limit_per_type = 100;
if (!page) page = 0;

const outgoingTxsResponse = await fetch(
`https://insight.thirdweb-dev.com/v1/transactions?chain=${chainId}&filter_from_address=${address}&page=${page}&limit=${limit_per_type}&sort_by=block_number&sort_order=desc`,
{
headers: {
"x-client-id": DASHBOARD_THIRDWEB_CLIENT_ID,
},
},
);

const incomingTxsResponse = await fetch(
`https://insight.thirdweb-dev.com/v1/transactions?chain=${chainId}&filter_to_address=${address}&page=${page}&limit=${limit_per_type}&sort_by=block_number&sort_order=desc`,
{
headers: {
"x-client-id": DASHBOARD_THIRDWEB_CLIENT_ID,
},
},
);

if (!outgoingTxsResponse.ok || !incomingTxsResponse.ok) {
throw new Error('Failed to fetch transaction history');
}

const outgoingTxsData: InsightsResponse = await outgoingTxsResponse.json();
const incomingTxsData: InsightsResponse = await incomingTxsResponse.json();

return [...outgoingTxsData.data, ...incomingTxsData.data].sort((a, b) => b.block_number - a.block_number);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {} from "@radix-ui/react-tabs";
import { useState } from "react";

interface Transaction {
id: number;
type: string;
id: string;
type: "out" | "in";
amount: string;
to?: string;
from?: string;
Expand All @@ -40,9 +40,18 @@ export function ActivityOverview({
contracts,
isLoading,
}: ActivityOverviewProps) {
const [activeTab, setActiveTab] = useState<"transactions" | "contracts">(
"transactions",
);
const [activeTab, setActiveTab] = useState<"transactions" | "contracts">("transactions");
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 5;

// Calculate the index of the last transaction on the current page
const lastIndex = currentPage * itemsPerPage;
// Calculate the index of the first transaction on the current page
const firstIndex = lastIndex - itemsPerPage;
// Get the current transactions to display
const currentTransactions = transactions.slice(firstIndex, lastIndex);
// Calculate total pages
const totalPages = Math.ceil(transactions.length / itemsPerPage);

return (
<Card>
Expand Down Expand Up @@ -71,31 +80,60 @@ export function ActivityOverview({
{isLoading ? (
<Spinner />
) : activeTab === "transactions" ? (
<Table>
<TableHeader>
<TableRow>
<TableHead>Type</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Details</TableHead>
<TableHead>Date</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{transactions.map((tx) => (
<TableRow key={tx.id}>
<TableCell>{tx.type}</TableCell>
<TableCell>{tx.amount}</TableCell>
<TableCell>
{tx.to && `To: ${tx.to}`}
{tx.from && `From: ${tx.from}`}
{tx.contract && `Contract: ${tx.contract}`}
{tx.method && ` Method: ${tx.method}`}
</TableCell>
<TableCell>{tx.date}</TableCell>
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>Type</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Details</TableHead>
<TableHead>Date</TableHead>
</TableRow>
))}
</TableBody>
</Table>
</TableHeader>
<TableBody>
{currentTransactions.map((tx) => (
<TableRow key={tx.id}>
<TableCell>{tx.type}</TableCell>
<TableCell>{tx.amount}</TableCell>
<TableCell>
{tx.to && `To: ${tx.to} `}
{tx.from && `From: ${tx.from} `}
{tx.contract && `Contract: ${tx.contract} `}
{tx.method && ` Method: ${tx.method}`}
</TableCell>
<TableCell>{tx.date}</TableCell>
</TableRow>
))}
</TableBody>
</Table>

{/* Pagination Controls */}
<div className="pagination">
<TabButtons
tabs={[
{
name: "Previous",
isActive: currentPage === 1,
isEnabled: currentPage > 1,
onClick: () => setCurrentPage((prev) => Math.max(prev - 1, 1)),
},
{
name: `Page ${currentPage} of ${totalPages}`,
isActive: true,
isEnabled: false,
onClick: () => {}, // No action needed
},
{
name: "Next",
isActive: currentPage === totalPages,
isEnabled: currentPage < totalPages,
onClick: () => setCurrentPage((prev) => Math.min(prev + 1, totalPages)),
},
]}
tabClassName="font-medium !text-sm"
/>
</div>
</>
) : activeTab === "contracts" ? (
<Table>
<TableHeader>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";
import type { ChainMetadata } from "thirdweb/chains";
import { useBalance } from "../hooks/getBalance";
import { useGetTxActivity } from "../hooks/useGetTxActivity";
import { useGetERC20Tokens } from "../hooks/useGetERC20Tokens";
import { useGetNFTs } from "../hooks/useGetNFTs";
import { mockWalletData } from "../utils/mockData";
Expand All @@ -23,19 +24,21 @@ export function WalletDashboard(props: {
const {
tokens,
isLoading: isLoadingERC20,
error: errorERC20,
// error: errorERC20,
} = useGetERC20Tokens(props.chain.chainId, props.address);
if (errorERC20) {
console.error("Error fetching ERC20 tokens:", errorERC20);
}
// if (errorERC20) {
// console.error("Error fetching ERC20 tokens:", errorERC20);
// }
const {
nfts,
isLoading: isLoadingNFTs,
error: errorNFTs,
// error: errorNFTs,
} = useGetNFTs(props.chain.chainId, props.address);
if (errorNFTs) {
console.error("Error fetching NFTs:", errorNFTs);
}
// if (errorNFTs) {
// console.error("Error fetching NFTs:", errorNFTs);
// }

const { txActivity, isLoading: isLoadingActivity } = useGetTxActivity(props.chain.chainId, props.address)

return (
<div className="grid gap-6">
Expand All @@ -45,7 +48,8 @@ export function WalletDashboard(props: {
isLoading={isLoadingBalance}
/>
<ActivityOverview
transactions={mockWalletData.transactions}
transactions={txActivity}
isLoading={isLoadingActivity}
contracts={mockWalletData.contracts}
/>
<TokenHoldings
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useEffect, useState } from "react";
import { fetchTxActivity } from "../actions/fetchTxActivity";

interface TxActivityItem {
id: string;
// all txs we retrieve for now are outgoing
// TODO: add incoming
type: "out" | "in";
amount: string;
to?: string;
from?: string;
method?: string;
date: string;
}

export function useGetTxActivity(chainId: number, address: string) {
const [txActivity, setTxActivity] = useState<TxActivityItem[]>([]);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
(async () => {
const response = await fetchTxActivity({ chainId, address });
const activity = response.map((tx): TxActivityItem => {
let type = tx.to_address?.toLowerCase() === address.toLowerCase() ? "in" : "out";
return {
id: tx.hash,
type,
amount: `${tx.value / Math.pow(10, 18)} ETH`,
to: tx.to_address || undefined,
from: tx.from_address,
method: tx.function_selector || undefined,
date: new Date(tx.block_timestamp * 1000).toLocaleString(),
};
})
setTxActivity(activity);
setIsLoading(false);
})();
}, [address, chainId]);

return { txActivity, isLoading };
}
Loading