diff --git a/tools/tenscan/frontend/api/transactions.ts b/tools/tenscan/frontend/api/transactions.ts index 7958581faf..051bf757ff 100644 --- a/tools/tenscan/frontend/api/transactions.ts +++ b/tools/tenscan/frontend/api/transactions.ts @@ -1,13 +1,15 @@ +import { jsonHexToObj } from "@/src/lib/utils"; import { httpRequest } from "."; -import { apiRoutes } from "@/src/routes"; +import { apiRoutes, ethMethods, tenCustomQueryMethods } from "@/src/routes"; import { pathToUrl } from "@/src/routes/router"; -import { ResponseDataInterface } from "@/src/types/interfaces"; +import { ResponseDataInterface, ToastType } from "@/src/types/interfaces"; import { TransactionCount, Price, TransactionResponse, Transaction, } from "@/src/types/interfaces/TransactionInterfaces"; +import { showToast } from "@/src/components/ui/use-toast"; export const fetchTransactions = async ( payload?: Record @@ -41,3 +43,52 @@ export const fetchTransactionByHash = async ( url: pathToUrl(apiRoutes.getTransactionByHash, { hash }), }); }; + +export const personalTransactionsData = async ( + provider: any, + walletAddress: string | null, + options: Record +) => { + try { + if (provider && walletAddress) { + const requestPayload = { + address: walletAddress, + pagination: { + ...options, + }, + }; + const personalTxResp = await provider.send(ethMethods.getStorageAt, [ + tenCustomQueryMethods.listPersonalTransactions, + JSON.stringify(requestPayload), + null, + ]); + const personalTxData = jsonHexToObj(personalTxResp); + return personalTxData; + } + + return null; + } catch (error) { + console.error("Error fetching personal transactions:", error); + showToast(ToastType.DESTRUCTIVE, "Error fetching personal transactions"); + throw error; + } +}; + +export const fetchPersonalTxnByHash = async ( + provider: any, + hash: string +): Promise => { + try { + if (provider) { + const personalTxnResp = await provider.send( + ethMethods.getTransactionReceipt, + [hash] + ); + return personalTxnResp; + } + } catch (error) { + console.error("Error fetching personal transaction:", error); + showToast(ToastType.DESTRUCTIVE, "Error fetching personal transaction"); + throw error; + } +}; diff --git a/tools/tenscan/frontend/pages/tx/[hash].tsx b/tools/tenscan/frontend/pages/tx/[hash].tsx index 8c7018e582..c47d0fb2ff 100644 --- a/tools/tenscan/frontend/pages/tx/[hash].tsx +++ b/tools/tenscan/frontend/pages/tx/[hash].tsx @@ -1,4 +1,3 @@ -import { fetchBatchByHash } from "@/api/batches"; import { fetchTransactionByHash } from "@/api/transactions"; import Layout from "@/src/components/layouts/default-layout"; import { TransactionDetailsComponent } from "@/src/components/modules/transactions/transaction-details"; @@ -9,7 +8,6 @@ import { CardHeader, CardTitle, CardContent, - CardDescription, } from "@/src/components/ui/card"; import { Skeleton } from "@/src/components/ui/skeleton"; import { useQuery } from "@tanstack/react-query"; @@ -28,30 +26,40 @@ export default function TransactionDetails() { return ( - {isLoading ? ( - - ) : transactionDetails ? ( - - - Transaction Details - - - - - - ) : ( - router.push("/transactions")}> - Go back - - } - /> - )} + + {isLoading ? ( + <> + + + + + + + + ) : transactionDetails ? ( + <> + + Transaction Details + + + + + + ) : ( + router.push("/transactions")}> + Go back + + } + className="p-8" + /> + )} + ); } diff --git a/tools/tenscan/frontend/pages/tx/personal/[hash].tsx b/tools/tenscan/frontend/pages/tx/personal/[hash].tsx new file mode 100644 index 0000000000..65e0c9889c --- /dev/null +++ b/tools/tenscan/frontend/pages/tx/personal/[hash].tsx @@ -0,0 +1,83 @@ +import Layout from "@/src/components/layouts/default-layout"; +import EmptyState from "@/src/components/modules/common/empty-state"; +import { Button } from "@/src/components/ui/button"; +import { + Card, + CardHeader, + CardTitle, + CardContent, +} from "@/src/components/ui/card"; +import { Skeleton } from "@/src/components/ui/skeleton"; +import { useQuery } from "@tanstack/react-query"; +import { useRouter } from "next/router"; +import { fetchPersonalTxnByHash } from "@/api/transactions"; +import { useWalletConnection } from "@/src/components/providers/wallet-provider"; +import { PersonalTxnDetailsComponent } from "@/src/components/modules/personal/personal-txn-details"; +import ConnectWalletButton from "@/src/components/modules/common/connect-wallet"; +import { ethereum } from "@/src/lib/utils"; + +export default function TransactionDetails() { + const router = useRouter(); + const { provider, walletConnected } = useWalletConnection(); + const { hash } = router.query; + + const { data: transactionDetails, isLoading } = useQuery({ + queryKey: ["personalTxnData", hash], + queryFn: () => fetchPersonalTxnByHash(provider, hash as string), + enabled: !!provider && !!hash, + }); + + return ( + + {walletConnected ? ( + isLoading ? ( + + ) : transactionDetails ? ( + + + Transaction Details + + + + + + ) : ( + router.push("/personal")}>Go back + } + /> + ) + ) : ( + + + + + } + /> + )} + + ); +} + +export async function getServerSideProps(context: any) { + return { + props: {}, + }; +} diff --git a/tools/tenscan/frontend/src/components/modules/common/data-table/data-table-pagination.tsx b/tools/tenscan/frontend/src/components/modules/common/data-table/data-table-pagination.tsx index d221bdf261..0a61d0f786 100644 --- a/tools/tenscan/frontend/src/components/modules/common/data-table/data-table-pagination.tsx +++ b/tools/tenscan/frontend/src/components/modules/common/data-table/data-table-pagination.tsx @@ -117,6 +117,10 @@ export function DataTablePagination({ setPage(table.getState().pagination.pageIndex + 1); table.nextPage(); }} + disabled={ + table.getState().pagination.pageSize > + table?.getFilteredRowModel()?.rows?.length + } // uncomment the following line when total count feature is implemented // disabled={!table.getCanNextPage()} > diff --git a/tools/tenscan/frontend/src/components/modules/common/empty-state.tsx b/tools/tenscan/frontend/src/components/modules/common/empty-state.tsx index 1a3a94cf9e..0372c45bd1 100644 --- a/tools/tenscan/frontend/src/components/modules/common/empty-state.tsx +++ b/tools/tenscan/frontend/src/components/modules/common/empty-state.tsx @@ -1,3 +1,4 @@ +import { cn } from "@/src/lib/utils"; import Image from "next/image"; import React from "react"; @@ -8,6 +9,7 @@ const EmptyState = ({ imageSrc, imageAlt, action, + className, }: { title?: string; description?: string; @@ -15,9 +17,15 @@ const EmptyState = ({ imageSrc?: string; imageAlt?: string; action?: React.ReactNode; + className?: string; }) => { return ( -
+
{icon &&
{icon}
} {imageSrc && ( @@ -25,6 +33,8 @@ const EmptyState = ({ src={imageSrc} alt={imageAlt || "Empty state"} className="w-24 h-24 rounded-full" + width={96} + height={96} /> )} {title && ( diff --git a/tools/tenscan/frontend/src/components/modules/dashboard/analytics-card.tsx b/tools/tenscan/frontend/src/components/modules/dashboard/analytics-card.tsx index 2caec11f59..41c6b6448e 100644 --- a/tools/tenscan/frontend/src/components/modules/dashboard/analytics-card.tsx +++ b/tools/tenscan/frontend/src/components/modules/dashboard/analytics-card.tsx @@ -21,10 +21,10 @@ export default function AnalyticsCard({
- {item.value ? ( - item.value - ) : ( + {item.loading ? ( + ) : ( + item.value )}
{item?.change && ( diff --git a/tools/tenscan/frontend/src/components/modules/dashboard/index.tsx b/tools/tenscan/frontend/src/components/modules/dashboard/index.tsx index f2ec71e079..18124148f6 100644 --- a/tools/tenscan/frontend/src/components/modules/dashboard/index.tsx +++ b/tools/tenscan/frontend/src/components/modules/dashboard/index.tsx @@ -39,9 +39,15 @@ interface RecentData { } export default function Dashboard() { - const { price, transactions, transactionCount } = useTransactionsService(); - const { contractCount } = useContractsService(); - const { batches, latestBatch } = useBatchesService(); + const { + price, + isPriceLoading, + transactions, + transactionCount, + isTransactionCountLoading, + } = useTransactionsService(); + const { contractCount, isContractCountLoading } = useContractsService(); + const { batches, latestBatch, isLatestBatchLoading } = useBatchesService(); const { rollups } = useRollupsService(); const DASHBOARD_DATA = [ @@ -53,6 +59,7 @@ export default function Dashboard() { // TODO: add change // change: "+20.1%", icon: RocketIcon, + loading: isPriceLoading, }, { title: "Latest L2 Batch", @@ -62,6 +69,7 @@ export default function Dashboard() { // TODO: add change // change: "+20.1%", icon: LayersIcon, + loading: isLatestBatchLoading, }, { title: "Latest L1 Rollup", @@ -77,6 +85,7 @@ export default function Dashboard() { // TODO: add change // change: "+20.1%", icon: CubeIcon, + loading: isLatestBatchLoading, }, { title: "Transactions", @@ -86,6 +95,7 @@ export default function Dashboard() { // TODO: add change // change: "+20.1%", icon: ReaderIcon, + loading: isTransactionCountLoading, }, { title: "Contracts", @@ -93,6 +103,7 @@ export default function Dashboard() { // TODO: add change // change: "+20.1%", icon: FileTextIcon, + loading: isContractCountLoading, }, { title: "Nodes", diff --git a/tools/tenscan/frontend/src/components/modules/personal/columns.tsx b/tools/tenscan/frontend/src/components/modules/personal/columns.tsx index 8bdbaa3fae..03fc288e3d 100644 --- a/tools/tenscan/frontend/src/components/modules/personal/columns.tsx +++ b/tools/tenscan/frontend/src/components/modules/personal/columns.tsx @@ -129,7 +129,7 @@ export const columns: ColumnDef[] = [ id: "actions", cell: ({ row }) => { return ( - + ); diff --git a/tools/tenscan/frontend/src/components/modules/personal/personal-txn-details.tsx b/tools/tenscan/frontend/src/components/modules/personal/personal-txn-details.tsx new file mode 100644 index 0000000000..00e174f8e6 --- /dev/null +++ b/tools/tenscan/frontend/src/components/modules/personal/personal-txn-details.tsx @@ -0,0 +1,233 @@ +import TruncatedAddress from "../common/truncated-address"; +import KeyValueItem, { KeyValueList } from "@/src/components/ui/key-value"; +import { Badge } from "@/src/components/ui/badge"; +import { + PersonalTransactionType, + TransactionReceipt, + TransactionType, +} from "@/src/types/interfaces/TransactionInterfaces"; +import { BadgeType } from "@/src/types/interfaces"; +import Link from "next/link"; + +export function PersonalTxnDetailsComponent({ + transactionDetails, +}: { + transactionDetails: TransactionReceipt; +}) { + const getTransactionType = (type: TransactionType) => { + switch (type) { + case PersonalTransactionType.Legacy: + return "Legacy"; + case PersonalTransactionType.AccessList: + return "Access List"; + case PersonalTransactionType.DynamicFee: + return "Dynamic Fee"; + case PersonalTransactionType.Blob: + return "Blob"; + default: + return "Unknown"; + } + }; + + return ( +
+ + + } + /> + + } + /> + + {Number(transactionDetails?.transactionIndex)} + + } + /> + + {getTransactionType(transactionDetails?.type)} + + } + /> + + {transactionDetails?.status ? "Success" : "Failed"} + + } + /> + + + {Number(transactionDetails?.blockNumber)} + + } + /> + + {Number(transactionDetails?.gasUsed)}{" "} + + } + /> + + {Number(transactionDetails?.cumulativeGasUsed)} + + } + /> + + {Number(transactionDetails?.effectiveGasPrice)} + + } + /> + } + /> + } + /> + + } + /> + + } + /> + + 0 ? ( +
+ {transactionDetails?.logs.map((log, index) => ( +
+ + } + /> + } + /> + + } + /> + + + {log.removed ? "Yes" : "No"} + + } + /> + + {log.topics.map((topic, index) => ( +
+ } + /> +
+ ))} +
+ } + /> + + } + /> + + {Number(transactionDetails?.transactionIndex)} + + } + isLastItem + /> + +
+ ))} +
+ ) : ( + "No logs found" + ) + } + isLastItem + /> + +
+ ); +} diff --git a/tools/tenscan/frontend/src/components/ui/key-value.tsx b/tools/tenscan/frontend/src/components/ui/key-value.tsx index d75ca31aea..540e3a7ce3 100644 --- a/tools/tenscan/frontend/src/components/ui/key-value.tsx +++ b/tools/tenscan/frontend/src/components/ui/key-value.tsx @@ -10,7 +10,7 @@ export const KeyValueItem = ({ value, isLastItem, }: { - label: string; + label?: string; value: string | number | React.ReactNode; isLastItem?: boolean; }) => ( @@ -19,7 +19,7 @@ export const KeyValueItem = ({ ${isLastItem ? "" : "mb-2"}`} >
- {label} + {label && {label}} {value}
{!isLastItem && } diff --git a/tools/tenscan/frontend/src/lib/constants.ts b/tools/tenscan/frontend/src/lib/constants.ts index c456eb7bd8..8c0808fdbd 100644 --- a/tools/tenscan/frontend/src/lib/constants.ts +++ b/tools/tenscan/frontend/src/lib/constants.ts @@ -18,7 +18,9 @@ const calculateOffset = (page: number, size: number) => { export const getOptions = (query: { page?: number; size?: number }) => { const defaultSize = 20; - const size = query.size ? (query.size > 100 ? 100 : query.size) : defaultSize; + const size = query.size + ? +(query.size > 100 ? 100 : query.size) + : defaultSize; const page = query.page || 1; const offset = calculateOffset(page, size); return { diff --git a/tools/tenscan/frontend/src/lib/utils.ts b/tools/tenscan/frontend/src/lib/utils.ts index d4cf0b32cc..a07a957028 100644 --- a/tools/tenscan/frontend/src/lib/utils.ts +++ b/tools/tenscan/frontend/src/lib/utils.ts @@ -49,3 +49,7 @@ export const getItem = ( return value; }; + +export function jsonHexToObj(hex: string) { + return JSON.parse(Buffer.from(hex.slice(2), "hex").toString()); +} diff --git a/tools/tenscan/frontend/src/routes/index.ts b/tools/tenscan/frontend/src/routes/index.ts index 61433a5d39..a85623ce77 100644 --- a/tools/tenscan/frontend/src/routes/index.ts +++ b/tools/tenscan/frontend/src/routes/index.ts @@ -37,6 +37,7 @@ export const apiRoutes = { export const ethMethods = { getStorageAt: "eth_getStorageAt", + getTransactionReceipt: "eth_getTransactionReceipt", }; // to send TEN Custom Queries (CQ) through the provider we call eth_getStorageAt and use these addresses to identify the TEN CQ method export const tenCustomQueryMethods = { diff --git a/tools/tenscan/frontend/src/services/useTransactionsService.ts b/tools/tenscan/frontend/src/services/useTransactionsService.ts index 5edbc3f345..5284c073f3 100644 --- a/tools/tenscan/frontend/src/services/useTransactionsService.ts +++ b/tools/tenscan/frontend/src/services/useTransactionsService.ts @@ -2,34 +2,22 @@ import { fetchEtherPrice, fetchTransactions, fetchTransactionCount, + personalTransactionsData, } from "@/api/transactions"; import { useWalletConnection } from "@/src/components/providers/wallet-provider"; import { useQuery } from "@tanstack/react-query"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { getOptions, pollingInterval, pricePollingInterval, } from "../lib/constants"; -import { PersonalTransactionsResponse } from "../types/interfaces/TransactionInterfaces"; import { useRouter } from "next/router"; -import { showToast } from "../components/ui/use-toast"; -import { ToastType } from "../types/interfaces"; -import {ethMethods, tenCustomQueryMethods} from "../routes"; export const useTransactionsService = () => { const { query } = useRouter(); const { walletAddress, provider } = useWalletConnection(); - const [personalTxnsLoading, setPersonalTxnsLoading] = useState(false); - const [personalTxns, setPersonalTxns] = - useState(); - - useEffect(() => { - personalTransactions(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [walletAddress]); - const [noPolling, setNoPolling] = useState(false); const options = getOptions(query); @@ -51,33 +39,11 @@ export const useTransactionsService = () => { refetchInterval: noPolling ? false : pollingInterval, }); - const personalTransactions = async () => { - try { - setPersonalTxnsLoading(true); - if (provider) { - const requestPayload = { - address: walletAddress, - pagination: { - ...options, - }, - }; - const personalTxResp = await provider.send(ethMethods.getStorageAt, [ - tenCustomQueryMethods.listPersonalTransactions, - JSON.stringify(requestPayload), - null, - ]); - const personalTxData = jsonHexToObj(personalTxResp); - setPersonalTxns(personalTxData); - } - } catch (error) { - console.error("Error fetching personal transactions:", error); - setPersonalTxns(undefined); - showToast(ToastType.DESTRUCTIVE, "Error fetching personal transactions"); - throw error; - } finally { - setPersonalTxnsLoading(false); - } - }; + const { data: personalTxns, isLoading: personalTxnsLoading } = useQuery({ + queryKey: ["personalTxns", options], + queryFn: () => personalTransactionsData(provider, walletAddress, options), + enabled: !!walletAddress && !!provider, + }); const { data: price, isLoading: isPriceLoading } = useQuery({ queryKey: ["price"], @@ -95,9 +61,6 @@ export const useTransactionsService = () => { personalTxns, personalTxnsLoading, price, + isPriceLoading, }; }; - -function jsonHexToObj(hex: string) { - return JSON.parse(Buffer.from(hex.slice(2), "hex").toString()); -} \ No newline at end of file diff --git a/tools/tenscan/frontend/src/types/interfaces/TransactionInterfaces.ts b/tools/tenscan/frontend/src/types/interfaces/TransactionInterfaces.ts index 8d468f3daa..25b8ef80c4 100644 --- a/tools/tenscan/frontend/src/types/interfaces/TransactionInterfaces.ts +++ b/tools/tenscan/frontend/src/types/interfaces/TransactionInterfaces.ts @@ -27,7 +27,14 @@ export type PersonalTransactionsResponse = { Total: number; }; -export type TransactionType = 0x0 | 0x1 | 0x2 | 0x3; +export type TransactionType = "0x0" | "0x1" | "0x2" | "0x3"; + +export enum PersonalTransactionType { + Legacy = "0x0", + AccessList = "0x1", + DynamicFee = "0x2", + Blob = "0x3", +} export type PersonalTransactions = { id: number; @@ -45,3 +52,32 @@ export type PersonalTransactions = { transactionIndex: string; type: TransactionType; }; + +export type TransactionReceipt = { + blockHash: string; + blockNumber: string; + contractAddress: string; + cumulativeGasUsed: string; + effectiveGasPrice: string; + from: string; + gasUsed: string; + logs: Log[]; + logsBloom: string; + status: string; + to: string; + transactionHash: string; + transactionIndex: string; + type: TransactionType; +}; + +export type Log = { + address: string; + blockHash: string; + blockNumber: string; + data: string; + logIndex: string; + removed: boolean; + topics: string[]; + transactionHash: string; + transactionIndex: string; +}; diff --git a/tools/tenscan/frontend/src/types/interfaces/index.ts b/tools/tenscan/frontend/src/types/interfaces/index.ts index 9ab1d962e6..48b87edb9e 100644 --- a/tools/tenscan/frontend/src/types/interfaces/index.ts +++ b/tools/tenscan/frontend/src/types/interfaces/index.ts @@ -93,6 +93,7 @@ export interface DashboardAnalyticsData { value: string | number | JSX.Element; change?: string; icon: any; + loading?: boolean; } export enum ItemPosition {