diff --git a/fuel-toolchain.toml b/fuel-toolchain.toml new file mode 100644 index 000000000..d541e585d --- /dev/null +++ b/fuel-toolchain.toml @@ -0,0 +1,6 @@ +[toolchain] +channel = "latest-2023-11-30" + +[components] +fuel-core = "0.24.3" +forc = "0.54.0" diff --git a/package.json b/package.json index 9576bd344..d89f07a6b 100644 --- a/package.json +++ b/package.json @@ -136,5 +136,8 @@ "ws@<8.17.1": ">=8.17.1", "fast-loops@<1.1.4": ">=1.1.4" } + }, + "dependencies": { + "recharts": "2.12.7" } } diff --git a/packages/app-explorer/package.json b/packages/app-explorer/package.json index 142cbe77f..a261e57a0 100644 --- a/packages/app-explorer/package.json +++ b/packages/app-explorer/package.json @@ -40,8 +40,10 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-json-view-lite": "1.4.0", + "react-modal": "3.16.1", "react-use": "17.5.0", "react-window": "1.8.10", + "recharts": "2.12.7", "tai64": "1.0.0", "tailwind-variants": "0.1.20", "wagmi": "2.12.7", diff --git a/packages/app-explorer/src/app/layout.tsx b/packages/app-explorer/src/app/layout.tsx index f3323df9f..82c379600 100644 --- a/packages/app-explorer/src/app/layout.tsx +++ b/packages/app-explorer/src/app/layout.tsx @@ -43,7 +43,7 @@ export default function RootLayout({ - + {children} diff --git a/packages/app-explorer/src/app/page.tsx b/packages/app-explorer/src/app/page.tsx index 5fb8a51d1..8c7e0a68e 100644 --- a/packages/app-explorer/src/app/page.tsx +++ b/packages/app-explorer/src/app/page.tsx @@ -1,8 +1,51 @@ +// 'use client'; +// import { GridTable } from '@fuels/ui'; +// import { Suspense, useState } from 'react'; +// import { columns, data } from '~/systems/Home/components/DataTable/data'; +// import { TxListLoader } from '~/systems/Transactions/components/TxList/TxListLoader'; +// import { TxsAccountTable } from '~/systems/Transactions/components/TxsAccontTable/TxsAccountTable'; +// import { TxsTitle } from '~/systems/Transactions/components/TxsTitle/TxsTitle'; +// import { TxsTokenTable } from '~/systems/Transactions/components/TxsTokenTable/TxsTokenTable'; +// import type { TxsRouteProps } from '~/systems/Transactions/types'; + +// export default function Home({ searchParams: { page = '1' } }: TxsRouteProps) { +// const [activeTab, setActiveTab] = useState('Top Tokens'); +// const [currentGridPage, setCurrentGridPage] = useState(0); +// return ( +// <> +// + +// } +// > +// {activeTab === 'Top Tokens' ? ( +// {}} +// data={data} +// columns={columns} +// pageCount={1} +// currentPage={currentGridPage} +// setCurrentPage={setCurrentGridPage} +// /> +// ) : activeTab === 'Top NFTs' ? ( +// +// ) : activeTab === 'Top Accounts' ? ( +// +// ) : ( +// '' +// )} +// +// +// ); +// } + import { Suspense } from 'react'; import { TxListLoader } from '~/systems/Transactions/components/TxList/TxListLoader'; import { TxsTitle } from '~/systems/Transactions/components/TxsTitle/TxsTitle'; import { TxsScreenSync } from '~/systems/Transactions/pages/TxsScreenSync'; import type { TxsRouteProps } from '~/systems/Transactions/types'; +// import { TxsTitle } from '~/systems/Transactions/components/TxsTitle/TxsTitle'; export default async function Home({ searchParams: { cursor, dir }, diff --git a/packages/app-explorer/src/systems/Core/components/Layout/Layout.tsx b/packages/app-explorer/src/systems/Core/components/Layout/Layout.tsx index 0041f44ed..55b3bd8de 100644 --- a/packages/app-explorer/src/systems/Core/components/Layout/Layout.tsx +++ b/packages/app-explorer/src/systems/Core/components/Layout/Layout.tsx @@ -23,7 +23,7 @@ export function Layout({ children, contentClassName }: LayoutProps) { diff --git a/packages/app-explorer/src/systems/Core/utils/sdk.ts b/packages/app-explorer/src/systems/Core/utils/sdk.ts index 43f1a5ec5..f30730925 100644 --- a/packages/app-explorer/src/systems/Core/utils/sdk.ts +++ b/packages/app-explorer/src/systems/Core/utils/sdk.ts @@ -1,8 +1,9 @@ import { getSdk } from '@fuel-explorer/graphql/sdk'; import { GraphQLClient } from 'graphql-request'; -const FUEL_INDEXER_API_KEY = process.env.FUEL_INDEXER_API_KEY; -const FUEL_INDEXER_API = process.env.FUEL_INDEXER_API; +const FUEL_INDEXER_API_KEY = 'nZ9GZayrd8'; +const FUEL_INDEXER_API = + 'https://explorer-indexer-testnet.fuel.network/graphql'; const FUEL_INDEXER_MAINNET_KEY = process.env.FUEL_INDEXER_MAINNET_KEY; if (!FUEL_INDEXER_API) { diff --git a/packages/app-explorer/src/systems/Home/components/BlockTableTile.tsx b/packages/app-explorer/src/systems/Home/components/BlockTableTile.tsx new file mode 100644 index 000000000..765594c7a --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/BlockTableTile.tsx @@ -0,0 +1,83 @@ +import { HStack, RoundedContainer, VStack } from '@fuels/ui'; +import { tv } from 'tailwind-variants'; +import { fromNow } from '~/systems/Core/utils/dayjs'; +import { Block } from '../interface/blocks.interface'; + +interface BlockTableProps { + block: Block; +} + +export const BlockTableTile: React.FC = ({ block }) => { + const classes = styles(); + + return ( + + +
+

#{block.blockNo}

+

{+block.gasUsed / 10 ** 9} ETH

+
+
+ {/* Uncomment the image if needed */} + {/* */} +

+ {block.producer} +

+
+ +
+ + + + + +

Settled

+
+

+ {fromNow(block.timeStamp)} +

+
+
+
+
+ ); +}; + +const styles = tv({ + slots: { + paragraphStrong: [ + 'text-[12px]', + 'text-[color:var(--gray-12)]', + 'font-bold', + ' w-[110px]', + ], + paragraph: [ + 'text-muted', + 'text-[12px]', + 'p-0', + 'whitespace-nowrap', + 'text-ellipsis', + ], + paragraphAccent: ['text-accent text-[12px] p-0'], + }, +}); diff --git a/packages/app-explorer/src/systems/Home/components/DailyTransaction.tsx b/packages/app-explorer/src/systems/Home/components/DailyTransaction.tsx new file mode 100644 index 000000000..ecd01fa41 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/DailyTransaction.tsx @@ -0,0 +1,134 @@ +import { ChartConfig, RoundedContainer } from '@fuels/ui'; +import dayjs from 'dayjs'; +import { + CartesianGrid, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +// import { Block } from '../interface/blocks.interface'; + +// const chartData = [ +// { time: '01:00', value: 100000 }, +// { time: '09:00', value: 500000 }, +// { time: '17:00', value: 1000000 }, +// { time: '23:00', value: 1500000 }, +// ]; + +const chartConfig = { + desktop: { + label: 'Desktop', + color: '#00F58C', + }, +} satisfies ChartConfig; + +interface DailyTransactionProps { + blocks: any; +} +const DailyTransaction = (blocks: DailyTransactionProps) => { + const chartData = blocks.blocks?.map((block: any) => ({ + time: dayjs(Number(block.time)).format('HH:mm'), + value: block.value, + })); + const cumilativeTsx = blocks.blocks.reduce( + (sum: any, block: any) => sum + Number(block.value), + 0, + ); + return ( + +
+
+
+
+
+ Daily Transactions + + + + + +
+
+ The total number of transactions completed on Fuel Network + within a 24-hour period. +
+
+
+
+ + 24h + +
+

+ {cumilativeTsx.toLocaleString()} +

+ + + + + + [`${Number(value)}`]} + labelFormatter={(label) => label.toLocaleString()} + contentStyle={{ + backgroundColor: 'var(--gray-1)', + borderColor: 'var(--gray-2)', + borderRadius: '8px', + color: 'var(--gray-1)', + }} + labelStyle={{ + color: 'var(--gray-12)', + fontWeight: 'bold', + }} + itemStyle={{ + color: '#00F58C', + }} + cursor={{ strokeWidth: 0.1, radius: 10 }} + /> + + + + +
+ + ); +}; + +export default DailyTransaction; diff --git a/packages/app-explorer/src/systems/Home/components/DataTable/GridTable.stories.tsx b/packages/app-explorer/src/systems/Home/components/DataTable/GridTable.stories.tsx new file mode 100644 index 000000000..264a226cb --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/DataTable/GridTable.stories.tsx @@ -0,0 +1,60 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import GridTable, { GridTableProps } from './GridTable'; +import { columns, data } from './data'; + +const meta: Meta = { + title: 'Home/GridTable', + component: GridTable, + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj>; + +export const Desktop: Story = { + args: { + columns, + data: data.slice(0, 10), + totalRows: data.length, + rowsPerPage: 10, + pageCount: Math.ceil(data.length / 10), + onPageChanged: (selectedPage: number) => + console.log(`Page changed to: ${selectedPage}`), + }, +}; + +export const Tablet: Story = { + args: { + columns, + data: data.slice(0, 10), + totalRows: data.length, + rowsPerPage: 10, + pageCount: Math.ceil(data.length / 10), + onPageChanged: (selectedPage: number) => + console.log(`Page changed to: ${selectedPage}`), + }, + parameters: { + viewport: { + defaultViewport: 'ipad', + }, + }, +}; + +export const Mobile: Story = { + args: { + columns, + data: data.slice(0, 10), + totalRows: data.length, + rowsPerPage: 10, + pageCount: Math.ceil(data.length / 10), + onPageChanged: (selectedPage: number) => + console.log(`Page changed to: ${selectedPage}`), + }, + parameters: { + viewport: { + defaultViewport: 'iphonex', + }, + }, +}; diff --git a/packages/app-explorer/src/systems/Home/components/DataTable/GridTable.tsx b/packages/app-explorer/src/systems/Home/components/DataTable/GridTable.tsx new file mode 100644 index 000000000..62f7ba152 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/DataTable/GridTable.tsx @@ -0,0 +1,131 @@ +import React, { useState } from 'react'; +import DataTable, { TableProps, TableColumn } from 'react-data-table-component'; +import ReactPaginate from 'react-paginate'; +import './gridTable.css'; + +export interface GridTableProps extends TableProps { + columns: TableColumn[]; + data: T[]; + pageCount: number; + onPageChanged: (selectedItem: number) => void; +} + +function GridTable({ + columns, + data, + pageCount, + onPageChanged, + ...props +}: GridTableProps): React.JSX.Element { + const [_currentPage, setCurrentPage] = useState(0); + + const customStyles = { + tableWrapper: { + style: { + borderRadius: '7px', + overflow: 'hidden', + }, + }, + table: { + style: { + backgroundColor: 'var(--gray-2)', + tableLayout: 'auto', + }, + }, + headRow: { + style: { + backgroundColor: 'var(--gray-2)', + color: '#9f9f9f', + fontWeight: '600', + }, + }, + headCells: { + style: { + backgroundColor: 'var(--gray-2)', + color: '#9f9f9f', + paddingLeft: '1rem', + paddingRight: '1rem', + paddingTop: '1.2rem', + paddingBottom: '1.2rem', + fontWeight: '600', + whiteSpace: 'nowrap', + }, + }, + rows: { + style: { + backgroundColor: 'var(--gray-2)', + fontWeight: '400', + }, + }, + cells: { + style: { + paddingLeft: '1rem', + paddingRight: '1rem', + color: 'var(--gray-table-text)', + paddingTop: '1.2rem', + paddingBottom: '1.2rem', + backgroundColor: 'var(--gray-2)', + fontWeight: '400', + whiteSpace: 'nowrap', + }, + }, + pagination: { + style: { + backgroundColor: 'var(--gray-2)', + color: '#f0f0f0', + }, + pageButtonsStyle: { + padding: '8px 16px', + margin: '0 4px', + color: '#f0f0f0', + borderRadius: '4px', + backgroundColor: 'var(--gray-2)', + '&.selected': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + fontWeight: 'bold', + }, + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.2)', + }, + }, + }, + }; + const Pagination: React.FC = () => { + return ( + ← Previous} // Left Arrow + nextLabel={Next →} // Right Arrow + breakLabel={'...'} + pageCount={pageCount} + marginPagesDisplayed={2} + pageRangeDisplayed={5} + onPageChange={handlePageClick} + containerClassName={'pagination'} + activeClassName={'selected'} + disabledClassName={'disabled'} // Handles styling for disabled state + pageLinkClassName={'page-link'} // Ensures consistent page link styling + /> + ); + }; + + const handlePageClick = (data: { selected: number }) => { + setCurrentPage(data.selected); + onPageChanged(data.selected); + }; + + return ( +
+ +
+ ); +} + +export default GridTable; diff --git a/packages/app-explorer/src/systems/Home/components/DataTable/data.ts b/packages/app-explorer/src/systems/Home/components/DataTable/data.ts new file mode 100644 index 000000000..af1edf92c --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/DataTable/data.ts @@ -0,0 +1,110 @@ +import { TableColumn } from 'react-data-table-component'; + +export interface RowData { + id: number; + name: string; + age: number; + email: string; + status: string; +} + +export const data: RowData[] = [ + { + id: 1, + name: 'John Doe', + age: 28, + email: 'john.doe@example.com', + status: 'Active', + }, + { + id: 2, + name: 'Jane Smith', + age: 34, + email: 'jane.smith@example.com', + status: 'Inactive', + }, + { + id: 3, + name: 'Michael Johnson', + age: 45, + email: 'michael.johnson@example.com', + status: 'Active', + }, + { + id: 4, + name: 'Emily Davis', + age: 23, + email: 'emily.davis@example.com', + status: 'Pending', + }, + { + id: 5, + name: 'William Brown', + age: 39, + email: 'william.brown@example.com', + status: 'Active', + }, + { + id: 6, + name: 'Olivia Taylor', + age: 29, + email: 'olivia.taylor@example.com', + status: 'Inactive', + }, + { + id: 7, + name: 'James Anderson', + age: 32, + email: 'james.anderson@example.com', + status: 'Pending', + }, + { + id: 8, + name: 'Sophia Thomas', + age: 27, + email: 'sophia.thomas@example.com', + status: 'Active', + }, + { + id: 9, + name: 'Isabella Lee', + age: 31, + email: 'isabella.lee@example.com', + status: 'Inactive', + }, + { + id: 10, + name: 'David Martinez', + age: 36, + email: 'david.martinez@example.com', + status: 'Active', + }, +]; + +export const columns: TableColumn[] = [ + { + name: 'Rank', + selector: (row) => row.id, + sortable: true, + }, + { + name: 'Token', + selector: (row) => row.name, + sortable: true, + }, + { + name: 'Price', + selector: (row) => row.age, + sortable: true, + }, + { + name: '24H Change (%)', + selector: (row) => row.email, + sortable: false, + }, + { + name: 'Volume (24H)', + selector: (row) => row.status, + sortable: true, + }, +]; diff --git a/packages/app-explorer/src/systems/Home/components/DataTable/gridTable.css b/packages/app-explorer/src/systems/Home/components/DataTable/gridTable.css new file mode 100644 index 000000000..3f6f23359 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/DataTable/gridTable.css @@ -0,0 +1,47 @@ +.pagination { + display: flex; + justify-content: end; + align-items: center; + list-style: none; + padding: 0; + margin: 1rem 0; + } + + .pagination li { + margin: 0 8px; /* Adjusts space between the pagination items */ + } + + .pagination li a { + padding: 8px 16px; /* Adjusts padding inside each pagination item */ + color: #f0f0f0; + background-color: rgb(32, 32, 32); + border-radius: 7px; + cursor: pointer; + text-decoration: none; + } + + .pagination li.selected a { + background-color: rgba(255, 255, 255, 0.2); /* Slightly lighter background */ + font-weight: bold; + } + + .pagination li a:hover { + background-color: rgba(255, 255, 255, 0.1); + } + + /* Remove background color for Previous and Next buttons */ + .pagination li.previous a, + .pagination li.next a { + background-color: transparent; + padding: 0; + } + + .pagination li.disabled a { + color: #888; /* Disabled color for previous/next buttons */ + cursor: not-allowed; + } + + .pagination li.disabled a:hover { + background-color: transparent; /* Keeps background same on hover */ + } + \ No newline at end of file diff --git a/packages/app-explorer/src/systems/Home/components/DataTable/index.tsx b/packages/app-explorer/src/systems/Home/components/DataTable/index.tsx new file mode 100644 index 000000000..9ab1264fb --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/DataTable/index.tsx @@ -0,0 +1,32 @@ +import { Button, Link, RoundedContainer, VStack } from '@fuels/ui'; + +import NextLink from 'next/link'; +import React from 'react'; +import { Block } from '../../interface/blocks.interface'; +import { BlockTableTile } from '../BlockTableTile'; +interface DataTableProps { + blocks: Block[]; +} + +export const DataTable = (props: DataTableProps) => { + return ( + + + {Array.from({ length: props.blocks.length }, (_, index) => ( + + + + ))} + + + + + + ); +}; diff --git a/packages/app-explorer/src/systems/Home/components/Epoch.tsx b/packages/app-explorer/src/systems/Home/components/Epoch.tsx new file mode 100644 index 000000000..f861265f7 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/Epoch.tsx @@ -0,0 +1,34 @@ +import { HStack, RoundedContainer } from '@fuels/ui'; + +const Epoch = () => { + return ( + +
+
+

+ Current EPOCH +

+
+ +

+ 657 +

+

+ 55% +

+
+
+
+
+
+
+
+ +
+ 0d 15h 32m 30s +
+ + ); +}; + +export default Epoch; diff --git a/packages/app-explorer/src/systems/Home/components/GasSaved/index.tsx b/packages/app-explorer/src/systems/Home/components/GasSaved/index.tsx new file mode 100644 index 000000000..06d65139b --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/GasSaved/index.tsx @@ -0,0 +1,108 @@ +import { ChartConfig, HStack, RoundedContainer } from '@fuels/ui'; +import { + CartesianGrid, + Line, + LineChart, + ResponsiveContainer, + XAxis, + YAxis, +} from 'recharts'; + +const chartData = [ + { time: '01:00', ETH: 0.1, FUEL: 0.12 }, + { time: '09:00', ETH: 0.5, FUEL: 0.48 }, + { time: '17:00', ETH: 0.9, FUEL: 0.85 }, + { time: '23:00', ETH: 1.2, FUEL: 1.15 }, +]; + +const chartConfig = { + eth: { + label: 'ETH', + color: '#00E8FC', // Light blue for ETH + }, + fuel: { + label: 'FUEL', + color: '#00F58C', // Light green for FUEL + }, +} satisfies ChartConfig; + +export const GasSaved = () => { + const numberFormatter = new Intl.NumberFormat('en-US', { + maximumFractionDigits: 1, + style: 'currency', + currency: 'USD', + }); + + return ( + +
+
+

+ Gas Used +

+ + 24h + +
+ +

+ $1.14 +

+

+ Per Transaction v ETH +

+
+ + + + + { + return numberFormatter.format(value); + }} + /> + + + + + +
+
+
+
+
ETH
+
+
+
+
FUEL
+
+
+ + ); +}; diff --git a/packages/app-explorer/src/systems/Home/components/GasSpentChart/index.tsx b/packages/app-explorer/src/systems/Home/components/GasSpentChart/index.tsx new file mode 100644 index 000000000..0110979b9 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/GasSpentChart/index.tsx @@ -0,0 +1,161 @@ +import { ChartConfig, HStack, RoundedContainer } from '@fuels/ui'; +import dayjs from 'dayjs'; +import { + CartesianGrid, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +const chartConfig = { + // eth: { + // label: 'ETH', + // color: '#00E8FC', // Light blue for ETH + // }, + fuel: { + label: 'FUEL', + color: '#00F58C', // Light green for FUEL + }, +} satisfies ChartConfig; + +interface GasSpentProps { + blocks: any; +} +export const GasSpentChart = (gasSpent: GasSpentProps) => { + const totalGasSpent = gasSpent.blocks + .map((block: any) => +block.value) + .reduce((acc: any, value: any) => acc + value, 0); + const chartData = gasSpent.blocks.map((e: any) => { + return { + time: dayjs(Number(e.time)).format('HH:mm'), + ETH: +e.value, + }; + }); + const minGasUsed = Math.min(...chartData.map((e: any) => e.ETH)) / 10 ** 9; + const maxGasUsed = Math.max(...chartData.map((e: any) => e.ETH)) / 10 ** 9; + + // const numberFormatter = new Intl.NumberFormat('en-US', { + // maximumFractionDigits: 1, + // style: 'currency', + // currency: 'USD', + // }); + + return ( + +
+
+
+
+
+ Gas Spent + + + + + +
+
+ The percentage of block resources utilized by transactions. +
+
+
+
+ + 24h + +
+ +

+ {(totalGasSpent / 10 ** 9).toFixed(8)} +

+

+ ETH +

+
+ + + + + [`${Number(value) / 10 ** 9} ETH`]} + labelFormatter={(label) => label.toLocaleString()} + contentStyle={{ + backgroundColor: 'var(--gray-1)', + borderColor: 'var(--gray-2)', + borderRadius: '8px', + color: 'var(--gray-1)', + }} + labelStyle={{ + color: 'var(--gray-12)', + fontWeight: 'bold', + }} + itemStyle={{ + color: '#00F58C', + }} + cursor={{ strokeWidth: 0.1, radius: 10 }} + /> + { + return Number((value / 10 ** 9).toFixed(6)).toExponential(); + }} + /> + + {/* */} + + + +
+
+ {/*
+
+
ETH
+
*/} + {/*
+
+
FUEL
+
*/} +
+ + ); +}; diff --git a/packages/app-explorer/src/systems/Home/components/GasTracker.tsx b/packages/app-explorer/src/systems/Home/components/GasTracker.tsx new file mode 100644 index 000000000..74e4775e2 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/GasTracker.tsx @@ -0,0 +1,78 @@ +import { RoundedContainer } from '@fuels/ui'; +import React from 'react'; + +export const GasTracker = () => { + return ( + +
+
+

+ Gas Tracker +

+
+ +
+
+
+ + + +

Fast

+
+

0.0000071 Ξ

+
+
+
+
+ + + + +

Medium

+
+

0.0000071 Ξ

+
+
+ +
+
+ + + + +

Slow

+
+

0.0000071 Ξ

+
+
+
+ + ); +}; diff --git a/packages/app-explorer/src/systems/Home/components/Hero/Hero.tsx b/packages/app-explorer/src/systems/Home/components/Hero/Hero.tsx index 9730ad906..6c1fc8caf 100644 --- a/packages/app-explorer/src/systems/Home/components/Hero/Hero.tsx +++ b/packages/app-explorer/src/systems/Home/components/Hero/Hero.tsx @@ -1,40 +1,212 @@ 'use client'; -import { Box, Container, Heading } from '@fuels/ui'; +import { Box, Container, Heading, Theme, VStack } from '@fuels/ui'; + +import { LoadingBox, LoadingWrapper } from '@fuels/ui'; +import { useEffect, useState } from 'react'; import { tv } from 'tailwind-variants'; -import { SearchWidget } from '~/systems/Core/components/Search/SearchWidget'; +import projectJson from '../../../../../../app-portal/src/systems/Ecosystem/data/projects.json'; +import { DataTable } from '../../components/DataTable'; +import { Block } from '../../interface/blocks.interface'; +import DailyTransaction from '../DailyTransaction'; +// import Epoch from '../Epoch'; +import { GasSpentChart } from '../GasSpentChart'; +// import { GasTracker } from '../GasTracker'; +import { LatestBlock } from '../LatestBlock'; +import { TPS } from '../TPS'; +import TotalDapps from '../TotalDapps/TotalDapps'; +import { getBlocksDashboard } from './actions/get-blocks-dashboard'; +import { getTPS } from './actions/get-tps'; export function Hero() { const classes = styles(); + const [isLoading, setIsLoading] = useState(true); + const [tpsData, setTpsData] = useState(null); + const [blocksData, setBlocksData] = useState(null); + const [isFirstFetch, setIsFirstFetch] = useState(true); + + const getTPSData = async () => { + try { + const result = await getTPS(); + const dashboard = await getBlocksDashboard(); + + setTpsData(result); + + setBlocksData(dashboard); + + if (isFirstFetch) { + setIsLoading(false); // Set loading to false only after first fetch + setIsFirstFetch(false); // Mark that the first fetch is done + } + } catch (_e) {} + }; + + useEffect(() => { + const interval = setInterval(() => { + getTPSData(); + }, 10000); + + return () => clearInterval(interval); + }, []); + + const blocks: Block[] = + blocksData?.getBlocksDashboard.nodes.map((node: any) => ({ + blockNo: node.blockNo ?? '', + producer: node.producer ?? '', + timeStamp: node.timestamp, + gasUsed: node.gasUsed, + tps: node.tps, + })) || []; + + const tps: any = + tpsData?.tps.nodes.map((node: any) => ({ + start: node.start ?? '', + end: node.end ?? '', + totalGas: node.totalGas, + txCount: node.txCount, + })) || []; + + const dailyTsxData = tps?.map((t: any) => ({ + time: t.start ?? '', + value: t.txCount, + })); + + console.log('the dailyTsx is ', dailyTsxData); + + const tpsTsxData = tps?.map((t: any) => ({ + time: t.start ?? '', + value: t.txCount, + })); + console.log('the tps6Tsx is ', tpsTsxData); + + const totalProjects = projectJson.length; + const activeProjects = projectJson.filter( + (item) => item.isLive === true, + ).length; + + const elementsWithImage = projectJson.filter((item) => item.image); + + const top3Projects = elementsWithImage + .filter((element) => element.isFeatured && element.isLive) + .slice(0, 3); return ( - - - - Explore Fuel - - - - - - + + + + + + Fuel Explorer + + {/* + Trending Items + +
+ +
*/} + + +
+ } + regularEl={} + /> +
+
+ } + regularEl={ + + } + /> +
+
+ } + regularEl={ + + } + /> +
+
+ } + regularEl={} + /> +
+ +
+ } + regularEl={} + /> + {/* */} +
+
+ } + regularEl={} + /> +
+
+
+
+
+
); } const styles = tv({ slots: { - root: 'hero-bg overflow-clip relative w-full border-b border-border', + root: 'overflow-clip relative w-full border-border bg-gray-3 dark:bg-gray-1', container: [ - 'z-20 relative py-8 pt-6 px-8 tablet:py-28 tablet:pt-24 tablet:px-10', - 'tablet:max-laptop:max-w-[500px] [&_.rt-ContainerInner]:p-2 [&_.rt-ContainerInner]:min-h-[120px]', - '[&_.rt-ContainerInner]:tablet:max-laptop:bg-black [&_.rt-ContainerInner]:tablet:max-laptop:bg-opacity-60 [&_.rt-ContainerInner]:tablet:max-laptop:rounded-lg [&_.rt-ContainerInner]:tablet:max-laptop:shadow-2xl', + 'z-20 relative py-8 pt-6 px-8 tablet:pt-18 tablet:px-10', + 'tablet:max-laptop:max-w-[500px] [&_.rt-ContainerInner]:p-2', + ' [&_.rt-ContainerInner]:tablet:max-laptop:bg-opacity-60 [&_.rt-ContainerInner]:tablet:max-laptop:rounded-lg [&_.rt-ContainerInner]:tablet:max-laptop:shadow-2xl', ], input: 'w-full tablet:w-[400px]', title: [ - 'text-2xl leading-snug text-white mb-4 justify-center', + 'text-2xl leading-snug text-heading justify-center', 'tablet:text-left tablet:text-4xl tablet:justify-start', ], subtitle: ['text-base mb-8 justify-center'], - searchWrapper: 'max-w-[400px]', + searchWrapper: [ + 'grid gap-5', + 'grid-cols-1 grid-rows-auto auto-rows-min', + 'md:grid-cols-1 md:grid-rows-[auto,auto]', + 'lg:grid-cols-12 lg:grid-rows-[repeat(4,auto)]', + 'gap-y-5 gap-x-4', + 'sm:grid-cols-1', + ], }, }); diff --git a/packages/graphql/logs/empty b/packages/app-explorer/src/systems/Home/components/Hero/Home/components/DailyTransaction.tsx similarity index 100% rename from packages/graphql/logs/empty rename to packages/app-explorer/src/systems/Home/components/Hero/Home/components/DailyTransaction.tsx diff --git a/packages/app-explorer/src/systems/Home/components/Hero/Home/components/Hero/Hero.stories.tsx b/packages/app-explorer/src/systems/Home/components/Hero/Home/components/Hero/Hero.stories.tsx new file mode 100644 index 000000000..51f030b43 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/Hero/Home/components/Hero/Hero.stories.tsx @@ -0,0 +1,36 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Hero } from './Hero'; + +const meta: Meta = { + title: 'Home/Hero', + component: Hero, + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Desktop: Story = { + args: {}, +}; + +export const Tablet: Story = { + args: {}, + parameters: { + viewport: { + defaultViewport: 'ipad', + }, + }, +}; + +export const Mobile: Story = { + args: {}, + parameters: { + viewport: { + defaultViewport: 'iphonex', + }, + }, +}; diff --git a/packages/app-explorer/src/systems/Home/components/Hero/actions/get-blocks-dashboard.ts b/packages/app-explorer/src/systems/Home/components/Hero/actions/get-blocks-dashboard.ts new file mode 100644 index 000000000..ac4622aa0 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/Hero/actions/get-blocks-dashboard.ts @@ -0,0 +1,6 @@ +import { sdk } from '~/systems/Core/utils/sdk'; + +export const getBlocksDashboard = async () => { + const { data } = await sdk.getBlocksDashboard(); + return data; +}; diff --git a/packages/app-explorer/src/systems/Home/components/Hero/actions/get-tps.ts b/packages/app-explorer/src/systems/Home/components/Hero/actions/get-tps.ts new file mode 100644 index 000000000..467c7a701 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/Hero/actions/get-tps.ts @@ -0,0 +1,6 @@ +import { sdk } from '~/systems/Core/utils/sdk'; + +export const getTPS = async () => { + const { data } = await sdk.tps(); + return data; +}; diff --git a/packages/app-explorer/src/systems/Home/components/LatestBlock.tsx b/packages/app-explorer/src/systems/Home/components/LatestBlock.tsx new file mode 100644 index 000000000..652b99032 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/LatestBlock.tsx @@ -0,0 +1,63 @@ +import { HStack, RoundedContainer, VStack } from '@fuels/ui'; + +import { fromNow } from '~/systems/Core/utils/dayjs'; +import { Block } from '../interface/blocks.interface'; + +export const LatestBlock = (block: Block) => { + return ( + +
+
+
+
+
+ Latest Block + + + + + +
+
+ The percentage of block resources utilized by transactions. +
+
+
+
+ + {fromNow(block?.timeStamp)} + +
+

+ {block.blockNo} +

+
+ + + +

+

+ {block.producer} +

+
+ +

+ Block Reward +

+

+ {(+block.gasUsed / 10 ** 9).toFixed(2)} ETH +

+
+
+ + ); +}; diff --git a/packages/app-explorer/src/systems/Home/components/TPS.tsx b/packages/app-explorer/src/systems/Home/components/TPS.tsx new file mode 100644 index 000000000..5d93d88b2 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/TPS.tsx @@ -0,0 +1,139 @@ +import { HStack, RoundedContainer } from '@fuels/ui'; +import dayjs from 'dayjs'; +import { + Bar, + BarChart, + CartesianGrid, + Cell, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +export interface TPSProps { + blocks: any; +} + +export const TPS = (props: TPSProps) => { + const blocks = props.blocks; + + const chartData = blocks?.map((block: any) => ({ + time: dayjs(Number(block.time)).format('HH:mm'), + value: block.value / 3600, + })); + + const averageTPS = + blocks.reduce((sum: any, block: any) => sum + Number(block.value), 0) / + blocks.length; + + const highestValue = Math.max( + ...chartData.map((data: any) => Number(data.value)), + ); + + const getTicks = () => { + const ticks: string[] = []; + for (let i = 0; i < chartData.length; i += 6) { + ticks.push(chartData[i].time); + } + return ticks; + }; + + return ( + +
+
+
+
+
+ TPS + + + + + +
+
+ Transactions Per Second processed by the Fuel network. +
+
+
+
+ + 24h + +
+ +

+ {`${Math.ceil(averageTPS / 3600)}`} +

+
TX/s
+
+ + + + + value} + /> + + [ + `${Number(value).toFixed(2)}`, + 'Avg TPS per hour', + ]} + labelFormatter={(label) => label.toLocaleString()} + contentStyle={{ + backgroundColor: 'var(--gray-1)', + borderColor: 'var(--gray-2)', + borderRadius: '8px', + color: 'var(--gray-1)', + }} + labelStyle={{ + color: 'var(--gray-12)', + fontWeight: 'bold', + }} + itemStyle={{ + color: '#00F58C', + }} + cursor={{ strokeWidth: 0.1, radius: 10 }} + /> + + {chartData.map((entry, index) => ( + + ))} + + + +
+ + ); +}; diff --git a/packages/app-explorer/src/systems/Home/components/TableCell/TableTile.jsx b/packages/app-explorer/src/systems/Home/components/TableCell/TableTile.jsx new file mode 100644 index 000000000..6224fd894 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/TableCell/TableTile.jsx @@ -0,0 +1,55 @@ +import { Flex, HStack, RoundedContainer } from '@fuels/ui'; +import { tv } from 'tailwind-variants'; + +export const TableTile = () => { + const classes = styles(); + + return ( + + +
+

#4540916

+

0.004878700 ETH

+
+
+ +

fuel1a...3zxt

+
+ +
+ + + + +

Settled

+
+

1 Hour Ago

+
+
+
+ ); +}; + +const styles = tv({ + slots: { + paragraphStrong: ['text-[14px]'], + paragraph: ['text-muted text-[14px] p-0'], + paragraphAccent: ['text-accent text-[14px] p-0'], + }, +}); diff --git a/packages/app-explorer/src/systems/Home/components/TotalDapps/DAppTile.tsx b/packages/app-explorer/src/systems/Home/components/TotalDapps/DAppTile.tsx new file mode 100644 index 000000000..d6a83c4fd --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/TotalDapps/DAppTile.tsx @@ -0,0 +1,23 @@ +import { HStack } from '@fuels/ui'; +import React from 'react'; + +export interface DAppTileProps { + name: string; + image: string; +} +const DAppTile = (props: DAppTileProps) => { + return ( + + {props.name} +

{props.name}

+
+ ); +}; + +export default DAppTile; diff --git a/packages/app-explorer/src/systems/Home/components/TotalDapps/TotalDapps.tsx b/packages/app-explorer/src/systems/Home/components/TotalDapps/TotalDapps.tsx new file mode 100644 index 000000000..f60e4eaee --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/TotalDapps/TotalDapps.tsx @@ -0,0 +1,142 @@ +import { RoundedContainer } from '@fuels/ui'; +import Link from 'next/link'; +import React from 'react'; + +interface ValidatorStatusProps { + active: number; + total: number; + featured: any; +} + +const TotalDapps: React.FC = ({ + active, + total, + featured, +}) => { + const activePercentage = (active / total) * 100; + const buildingPercentage = ((total - active) / total) * 100; + + const activeBarStyle = { + width: `${activePercentage}%`, + height: '5px', + borderRadius: '4px', + transition: 'width 0.4s ease-in-out', + }; + + const buildingBarStyle = { + width: `${buildingPercentage}%`, + height: '5px', + borderRadius: '4px', + transition: 'width 0.4s ease-in-out', + }; + const _image = 'zap'; + + return ( + +
+

+ Fuel Dapps +

+ + View All + +
+

+ {total} +

+ +
+
+
+
+
+
+
+
+ + Active: {active} + + + Building: {total - active} + +
+ +
+ + + Featured Dapps + + + {featured.map((feature: any) => { + return ( + + +

{feature.name}

+ + ); + })} +
+ + ); +}; +; +export default TotalDapps; diff --git a/packages/app-explorer/src/systems/Home/components/TrendingCard/TrendingCard.tsx b/packages/app-explorer/src/systems/Home/components/TrendingCard/TrendingCard.tsx new file mode 100644 index 000000000..65ae217ca --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/TrendingCard/TrendingCard.tsx @@ -0,0 +1,31 @@ +import { RoundedContainer } from '@fuels/ui'; +import { tv } from 'tailwind-variants'; + +export interface TrendingCardProps { + title: string; + icon: string; +} + +export const TrendingCard = ({ icon, title }: TrendingCardProps) => { + const classes = styles(); + return ( + +
+ {title} +

{title}

+
+
+ ); +}; + +const styles = tv({ + slots: { + paragraphStrong: ['text-sm px-2 whitespace-nowrap'], + }, +}); diff --git a/packages/app-explorer/src/systems/Home/components/TrendingCardCarousel.tsx b/packages/app-explorer/src/systems/Home/components/TrendingCardCarousel.tsx new file mode 100644 index 000000000..964461392 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/TrendingCardCarousel.tsx @@ -0,0 +1,132 @@ +'use client'; + +import { Button, Carousel, CarouselContent, CarouselItem } from '@fuels/ui'; +import { CarouselApi } from '@fuels/ui/src/components/Carousel/Carousel'; +import React from 'react'; +import { TrendingCard } from '../components/TrendingCard/TrendingCard'; + +export const TRENDING_DATA = [ + { + title: 'Bored Ape', + icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__', + }, + { + title: 'Bored Ape', + icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__', + }, + { + title: 'Bored Ape', + icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__', + }, + { + title: 'Bored Ape', + icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__', + }, + { + title: 'Bored Ape', + icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__', + }, + { + title: 'Bored Ape', + icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__', + }, + { + title: 'Bored Ape', + icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__', + }, + { + title: 'Bored Ape', + icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__', + }, + { + title: 'Bored Ape', + icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__', + }, + { + title: 'Bored Ape', + icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__', + }, +]; + +export const TrendingCardCarousel = () => { + const [api, setApi] = React.useState(); + const [current, setCurrent] = React.useState(0); + const [count, setCount] = React.useState(0); + + React.useEffect(() => { + if (!api) { + return; + } + + setCount(api.scrollSnapList().length); + setCurrent(api.selectedScrollSnap() + 1); + + api.on('select', () => { + setCurrent(api.selectedScrollSnap() + 1); + }); + }, [api]); + + return ( +
+ {/* Mext button */} + {current < count && ( + + )} + {/* Previous Button */} + {current > 1 && ( + + )} + + + {TRENDING_DATA.map((item, i) => ( + + + + ))} + + +
+ ); +}; diff --git a/packages/app-explorer/src/systems/Home/components/ViewButton.tsx b/packages/app-explorer/src/systems/Home/components/ViewButton.tsx new file mode 100644 index 000000000..7717b7987 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/ViewButton.tsx @@ -0,0 +1,34 @@ +import { HStack, RoundedContainer } from '@fuels/ui'; +import { tv } from 'tailwind-variants'; + +export const ViewAllButton = () => { + const classes = styles(); + return ( + + +

View All

+ + + +
+
+ ); +}; + +const styles = tv({ + slots: { + paragraphStrong: ['text-[14px] p-0'], + }, +}); diff --git a/packages/app-explorer/src/systems/Home/interface/blocks.interface.ts b/packages/app-explorer/src/systems/Home/interface/blocks.interface.ts new file mode 100644 index 000000000..96c58b59d --- /dev/null +++ b/packages/app-explorer/src/systems/Home/interface/blocks.interface.ts @@ -0,0 +1,6 @@ +export interface Block { + blockNo: string; + producer: string; + timeStamp: string; + gasUsed: string; +} diff --git a/packages/app-explorer/src/systems/Statistics/getAccounts.ts b/packages/app-explorer/src/systems/Statistics/getAccounts.ts new file mode 100644 index 000000000..6d130f58f --- /dev/null +++ b/packages/app-explorer/src/systems/Statistics/getAccounts.ts @@ -0,0 +1,137 @@ +'use server'; + +import { z } from 'zod'; +import { act } from '../../systems/Core/utils/act-server'; +import { sdk } from '../../systems/Core/utils/sdk'; +import { + createIntervals, + getUnitAndInterval, + processAccounts, // You would need to implement this to handle account-specific data +} from '../utils/utils'; + +const schema = z.object({ + timeFilter: z.string().optional().nullable(), +}); + +interface AccountParams { + timeFilter?: string; +} + +interface AccountNode { + __typename: 'Account'; + timestamp: string; +} + +// Common function to process account statistics +async function fetchAccountStatistics( + params: AccountParams, + fieldName: + | 'newAccountStatistics' + | 'cumulativeAccountCreationStatistics' + | 'dailyActiveAccounts', + unit: 'minute' | 'hour' | 'day' | 'month', + intervalSize: number, + isCumulative = false, +) { + const data = await (fieldName === 'dailyActiveAccounts' + ? sdk.dailyActiveAccounts(params) + : isCumulative + ? sdk.cumulativeAccountCreationStatistics(params) + : sdk.newAccountStatistics(params)); + + const { nodes, offset } = extractAccountData(data, fieldName); + if (!nodes.length) { + return isCumulative ? { accounts: [], offset: 0 } : { accounts: [] }; + } + + const firstTimestamp = Number(nodes[0].timestamp); + const lastTimestamp = Number(nodes[nodes.length - 1].timestamp); + + const intervalMap = createIntervals( + firstTimestamp, + lastTimestamp, + unit, + intervalSize, + ); + const accounts = processAccounts(nodes, intervalMap); + + if (isCumulative) { + return { accounts, offset }; + } + + return accounts; +} + +// Helper to extract nodes and timestamps from the response +function extractAccountData( + data: any, + fieldName: string, +): { nodes: AccountNode[]; offset?: number } { + const nodes = data.data[fieldName]?.nodes || []; + const offset = data.data[fieldName]?.accountOffset; + return { nodes, offset }; +} + +async function getDailyActiveAccountStats( + params: AccountParams, + unit: 'minute' | 'hour' | 'day' | 'month', + intervalSize: number, +) { + return fetchAccountStatistics( + params, + 'dailyActiveAccounts', // Specify the new field for daily active accounts + unit, + intervalSize, + ); +} + +async function getCumulativeAccountCreationStats( + params: AccountParams, + unit: 'minute' | 'hour' | 'day' | 'month', + intervalSize: number, +) { + return fetchAccountStatistics( + params, + 'cumulativeAccountCreationStatistics', + unit, + intervalSize, + true, + ); +} + +async function getNewAccountCreationStats( + params: AccountParams, + unit: 'minute' | 'hour' | 'day' | 'month', + intervalSize: number, +) { + return fetchAccountStatistics( + params, + 'newAccountStatistics', + unit, + intervalSize, + ); +} + +export const getDailyActiveAccountStatsAction = act( + schema, + async ({ timeFilter }) => { + const params = { timeFilter: timeFilter } as { timeFilter?: string }; + const { unit, intervalSize } = getUnitAndInterval(params.timeFilter || ''); + + return getDailyActiveAccountStats(params, unit, intervalSize); + }, +); + +export const getNewAccountStats = act(schema, async ({ timeFilter }) => { + const params = { timeFilter: timeFilter } as { timeFilter?: string }; + const { unit, intervalSize } = getUnitAndInterval(params.timeFilter || ''); + + return getNewAccountCreationStats(params, unit, intervalSize); +}); + +export const getCumulativeAccountStats = act(schema, async ({ timeFilter }) => { + const params = { timeFilter: timeFilter } as { timeFilter?: string }; + const { unit, intervalSize } = getUnitAndInterval(params.timeFilter || ''); + + return getCumulativeAccountCreationStats(params, unit, intervalSize); +}); diff --git a/packages/app-explorer/src/systems/Statistics/getBlocks.ts b/packages/app-explorer/src/systems/Statistics/getBlocks.ts new file mode 100644 index 000000000..1eba670f1 --- /dev/null +++ b/packages/app-explorer/src/systems/Statistics/getBlocks.ts @@ -0,0 +1,62 @@ +'use server'; + +import { z } from 'zod'; +import { act } from '../../systems/Core/utils/act-server'; +import { sdk } from '../../systems/Core/utils/sdk'; +import { DateHelper } from '../utils/date'; +import { createIntervals, getUnitAndInterval } from '../utils/utils'; + +const schema = z.object({ + timeFilter: z.string().optional().nullable(), +}); + +export const getBlockStats = act(schema, async ({ timeFilter }) => { + const params = { timeFilter: timeFilter } as { + timeFilter?: string; + }; + const data = await sdk.blockRewardStatistics(params); + + if (!data.data.blockRewardStatistics.nodes) { + return {}; + } + const { unit, intervalSize } = getUnitAndInterval(params.timeFilter || ''); + const nodes = data.data.blockRewardStatistics.nodes; + const firstTimestamp = Number(DateHelper.tai64toDate(nodes[0].timestamp)); + const lastTimestamp = Number( + DateHelper.tai64toDate(nodes[nodes.length - 1].timestamp), + ); + + const intervals = createIntervals( + firstTimestamp, + lastTimestamp, + unit, + intervalSize, + ); + + const intervalMap = intervals.map((interval) => ({ + start: interval.start.toISOString(), + end: interval.end.toISOString(), + count: 0, + totalRewards: 0, + })); + + // Process blocks and put them into the correct interval + nodes.forEach((block) => { + const blockTimestamp = Number(DateHelper.tai64toDate(block.timestamp)); + const blockReward = Number(block.reward); + + // Find the correct interval for the current block + for (const interval of intervalMap) { + const intervalStart = new Date(interval.start).getTime(); + const intervalEnd = new Date(interval.end).getTime(); + + if (blockTimestamp >= intervalStart && blockTimestamp < intervalEnd) { + // Increment count and add the reward to totalRewards + interval.count += 1; + interval.totalRewards += blockReward; + break; // Block has been assigned to the correct interval, no need to check further + } + } + }); + return intervalMap; +}); diff --git a/packages/app-explorer/src/systems/Statistics/getExportData.ts b/packages/app-explorer/src/systems/Statistics/getExportData.ts new file mode 100644 index 000000000..d64ff58f7 --- /dev/null +++ b/packages/app-explorer/src/systems/Statistics/getExportData.ts @@ -0,0 +1,40 @@ +'use server'; + +import { z } from 'zod'; +import { act } from '../../systems/Core/utils/act-server'; +import { sdk } from '../../systems/Core/utils/sdk'; + +const schema = z.object({ + account: z.string().optional().nullable(), + startDate: z.string(), + endDate: z.string(), +}); + +export const getTransactionStats = act( + schema, + async ({ account, startDate, endDate }) => { + if (!account) { + throw new Error('Account is required'); + } + + // Prepare parameters for the GraphQL query + const params = { + account, + startDate, + endDate, + }; + + // Call the getTransactionsByAccountAndDate function from the sdk + const data = await sdk.getTransactionsByAccountAndDate(params); + + // Check if any transactions are available + if (!data.data.transactionsByAccountAndDate) { + return []; + } + + // Destructure transactions and return the raw data + const transactions = data.data.transactionsByAccountAndDate; + + return transactions; + }, +); diff --git a/packages/app-explorer/src/systems/Statistics/getTopAccounts.ts b/packages/app-explorer/src/systems/Statistics/getTopAccounts.ts new file mode 100644 index 000000000..11393c06e --- /dev/null +++ b/packages/app-explorer/src/systems/Statistics/getTopAccounts.ts @@ -0,0 +1,75 @@ +'use server'; + +import { z } from 'zod'; +import { act } from '../../systems/Core/utils/act-server'; +import { sdk } from '../../systems/Core/utils/sdk'; + +// Schema to validate inputs +const schema = z.object({ + sortBy: z.string().optional(), // Sorting criteria (balance, transaction_count, etc.) + sortOrder: z.string().optional(), // asc or desc + first: z.number().optional().nullable(), // Number of accounts to fetch, can be null + cursor: z.string().optional().nullable(), // Pagination cursor +}); + +// Common function to fetch top accounts +async function fetchTopAccounts( + sortBy = 'transaction_count', // Default to transaction_count + sortOrder: 'asc' | 'desc' = 'desc', // Default to descending order + first: number | null = null, // Allow null to fetch all records if no limit is provided + cursor?: string | null, // Cursor for pagination (optional) +) { + const queryParams: Record = { + sortBy, + sortOrder, + cursor, + }; + + // If `first` is provided and not null, add it to the query parameters + if (first !== null) { + queryParams.first = first; + } + + const data = await sdk.paginatedAccounts(queryParams); + + // Handle case where no data is returned + if (!data.data.paginatedAccounts.nodes.length) { + return { + accounts: [], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }, + }; + } + + const { nodes, pageInfo } = data.data.paginatedAccounts; + + const accounts = nodes.map((account: any) => ({ + id: account.id, + account_id: account.account_id, + balance: account.balance, + transaction_count: account.transaction_count, + })); + + return { + accounts, + pageInfo: { + hasNextPage: pageInfo.hasNextPage, + hasPreviousPage: pageInfo.hasPreviousPage, + startCursor: pageInfo.startCursor, + endCursor: pageInfo.endCursor, + }, + }; +} + +export const getTopAccounts = act(schema, async (params) => { + const sortBy = params.sortBy || 'transaction_count'; + const sortOrder = (params.sortOrder || 'desc') as 'asc' | 'desc'; + const first = params.first === null ? null : params.first || 10; + const cursor = params.cursor || null; + + return fetchTopAccounts(sortBy, sortOrder, first, cursor); +}); diff --git a/packages/app-explorer/src/systems/Statistics/getTransactions.ts b/packages/app-explorer/src/systems/Statistics/getTransactions.ts new file mode 100644 index 000000000..9b5657346 --- /dev/null +++ b/packages/app-explorer/src/systems/Statistics/getTransactions.ts @@ -0,0 +1,131 @@ +'use server'; + +import { z } from 'zod'; +import { act } from '../../systems/Core/utils/act-server'; +import { sdk } from '../../systems/Core/utils/sdk'; +import { DateHelper } from '../utils/date'; +import { + createIntervals, + getUnitAndInterval, + processTransactions, +} from '../utils/utils'; + +const schema = z.object({ + timeFilter: z.string().optional().nullable(), +}); + +interface TransactionParams { + timeFilter?: string; +} + +interface TransactionNode { + __typename: 'TransactionFee'; + fee: string; + timestamp: string; +} + +// Common function to process transaction statistics +async function fetchTransactionStatistics( + params: TransactionParams, + fieldName: + | 'transactionsFeeStatistics' + | 'cumulativeTransactionsFeeStatistics', + unit: 'minute' | 'hour' | 'day' | 'month', + intervalSize: number, + isCumulative = false, +) { + const data = await (isCumulative + ? sdk.cumulativeTransactionsFeeStatistics(params) + : sdk.transactionsFeeStatistics(params)); + + const { nodes, offset } = extractTransactionData(data, fieldName); + + if (!nodes.length) { + return isCumulative + ? { transactions: [], offset: 0 } + : { transactions: [] }; + } + + const firstTimestamp = Number(DateHelper.tai64toDate(nodes[0].timestamp)); + const lastTimestamp = Number( + DateHelper.tai64toDate(nodes[nodes.length - 1].timestamp), + ); + + const intervalMap = createIntervals( + firstTimestamp, + lastTimestamp, + unit, + intervalSize, + ); + const transactions = processTransactions(nodes, intervalMap); + + if (isCumulative) { + return { transactions, offset }; + } + + return transactions; +} + +// Helper to extract nodes and timestamps from the response +function extractTransactionData( + data: any, + fieldName: string, +): { nodes: TransactionNode[]; offset?: number } { + const nodes = data.data[fieldName]?.nodes || []; + const offset = data.data[fieldName]?.transactionOffset; + return { nodes, offset }; +} + +async function getCumulativeTransactionFeeStats( + params: TransactionParams, + unit: 'minute' | 'hour' | 'day' | 'month', + intervalSize: number, +) { + return fetchTransactionStatistics( + params, + 'cumulativeTransactionsFeeStatistics', + unit, + intervalSize, + true, + ); +} + +export async function getTransactionFeeStats( + params: TransactionParams, + unit: 'minute' | 'hour' | 'day' | 'month', + intervalSize: number, +) { + return fetchTransactionStatistics( + params, + 'transactionsFeeStatistics', + unit, + intervalSize, + ); +} + +export const getTransactionStats = act(schema, async ({ timeFilter }) => { + const params = { timeFilter: timeFilter } as { timeFilter?: string }; + const { unit, intervalSize } = getUnitAndInterval(params.timeFilter || ''); + + return getTransactionFeeStats(params, unit, intervalSize); +}); + +export const getDailyTransactionFeeStats = act( + schema, + async ({ timeFilter }) => { + const params = { timeFilter: timeFilter } as { timeFilter?: string }; + + // Use 'day' as the unit and 1 as the interval size + return getTransactionFeeStats(params, 'day', 1); + }, +); + +export const getCumulativeTransactionStats = act( + schema, + async ({ timeFilter }) => { + const params = { timeFilter: timeFilter } as { timeFilter?: string }; + const { unit, intervalSize } = getUnitAndInterval(params.timeFilter || ''); + + return getCumulativeTransactionFeeStats(params, unit, intervalSize); + }, +); diff --git a/packages/app-explorer/src/systems/Transactions/components/ModalContainer/ModalContainer.tsx b/packages/app-explorer/src/systems/Transactions/components/ModalContainer/ModalContainer.tsx new file mode 100644 index 000000000..b53d6acb0 --- /dev/null +++ b/packages/app-explorer/src/systems/Transactions/components/ModalContainer/ModalContainer.tsx @@ -0,0 +1,28 @@ +'use client'; +import React from 'react'; +import Modal from 'react-modal'; +// Bind modal to your appElement (for accessibility) + +const ModalContainer = ({ + isOpen, + onRequestClose, + children, +}: { + isOpen: boolean; + onRequestClose: () => void; + children: React.ReactNode; +}) => { + return ( + + {children} + + ); +}; + +export default ModalContainer; diff --git a/packages/app-explorer/src/systems/Transactions/components/NFTHashItem/NFTHashItem.tsx b/packages/app-explorer/src/systems/Transactions/components/NFTHashItem/NFTHashItem.tsx new file mode 100644 index 000000000..d2cc486b9 --- /dev/null +++ b/packages/app-explorer/src/systems/Transactions/components/NFTHashItem/NFTHashItem.tsx @@ -0,0 +1,22 @@ +import { Box, Copyable, HStack, VStack } from '@fuels/ui'; + +type NFTHashItemProps = { + hashAddress: string; + width: string; +}; + +export default function NFTHashItem({ hashAddress, width }: NFTHashItemProps) { + return ( + + + + +

+ {hashAddress} +

+
+
+
+
+ ); +} diff --git a/packages/app-explorer/src/systems/Transactions/components/NFTProfileHolderIcon/NFTProfileHolderIcon.tsx b/packages/app-explorer/src/systems/Transactions/components/NFTProfileHolderIcon/NFTProfileHolderIcon.tsx new file mode 100644 index 000000000..970cfd2ed --- /dev/null +++ b/packages/app-explorer/src/systems/Transactions/components/NFTProfileHolderIcon/NFTProfileHolderIcon.tsx @@ -0,0 +1,14 @@ +import { HStack, Text } from '@fuels/ui'; + +export default function NFTProfileHolderIcon() { + return ( + + profile_image + 0x52....13d1 + + ); +} diff --git a/packages/app-explorer/src/systems/Transactions/components/NFTProfileIcon/NFTProfileIcon.tsx b/packages/app-explorer/src/systems/Transactions/components/NFTProfileIcon/NFTProfileIcon.tsx new file mode 100644 index 000000000..33ff43bc8 --- /dev/null +++ b/packages/app-explorer/src/systems/Transactions/components/NFTProfileIcon/NFTProfileIcon.tsx @@ -0,0 +1,14 @@ +import { HStack, Text } from '@fuels/ui'; + +export default function NFTProfileIcon() { + return ( + + profile_image + DeGod #5990 + + ); +} diff --git a/packages/app-explorer/src/systems/Transactions/components/NFTitem/NFTitem.tsx b/packages/app-explorer/src/systems/Transactions/components/NFTitem/NFTitem.tsx new file mode 100644 index 000000000..c071922ce --- /dev/null +++ b/packages/app-explorer/src/systems/Transactions/components/NFTitem/NFTitem.tsx @@ -0,0 +1,19 @@ +import { Box, Copyable, HStack, Text, VStack } from '@fuels/ui'; + +export default function NFTItem() { + return ( + + + + + #275029958 + + + + 0.004878700 ETH + + ); +} diff --git a/packages/app-explorer/src/systems/Transactions/components/NFTsHeader/NFTsHeader.tsx b/packages/app-explorer/src/systems/Transactions/components/NFTsHeader/NFTsHeader.tsx new file mode 100644 index 000000000..e445bfc70 --- /dev/null +++ b/packages/app-explorer/src/systems/Transactions/components/NFTsHeader/NFTsHeader.tsx @@ -0,0 +1,28 @@ +import { RoundedContainer } from '@fuels/ui'; +import React from 'react'; + +interface LineGraphProps { + titleProp: string; + valuesProp: string; + timeProp: string; +} + +export const NFTsHeader: React.FC = ({ + titleProp, + valuesProp, + timeProp, +}) => { + return ( + +

+ {titleProp} +

+

+ {valuesProp} +

+

+ {timeProp} +

+
+ ); +}; diff --git a/packages/app-explorer/src/systems/Transactions/components/TxList/TxListLoader.tsx b/packages/app-explorer/src/systems/Transactions/components/TxList/TxListLoader.tsx index 33b5d798f..4fd5a8ea6 100644 --- a/packages/app-explorer/src/systems/Transactions/components/TxList/TxListLoader.tsx +++ b/packages/app-explorer/src/systems/Transactions/components/TxList/TxListLoader.tsx @@ -1,3 +1,24 @@ +// import type { GQLRecentTransactionsQuery } from '@fuel-explorer/graphql'; +// import { TxList } from './TxList'; + +// type TxListLoaderProps = { +// numberOfTxs?: number; +// page: string; +// }; + +// export const TxListLoader = ({ numberOfTxs = 4, page }: TxListLoaderProps) => { +// console.log(page); +// const baseArray = Array.from( +// { length: numberOfTxs }, +// (_, index) => index + 1, +// ); +// const txs = baseArray.map((v) => ({ +// id: `${v}`, +// })) as GQLRecentTransactionsQuery['transactions']['nodes']; + +// return ; +// }; + import type { GQLRecentTransactionsQuery } from '@fuel-explorer/graphql'; import { TxList } from './TxList'; diff --git a/packages/app-explorer/src/systems/Transactions/components/TxsAccontTable/TxsAccountTable.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsAccontTable/TxsAccountTable.tsx new file mode 100644 index 000000000..0987fa0e0 --- /dev/null +++ b/packages/app-explorer/src/systems/Transactions/components/TxsAccontTable/TxsAccountTable.tsx @@ -0,0 +1,145 @@ +import { GridTable } from '@fuels/ui'; +import { useState } from 'react'; +import NFTHashItem from '../NFTHashItem/NFTHashItem'; +import NFTProfileHolderIcon from '../NFTProfileHolderIcon/NFTProfileHolderIcon'; +import NFTItem from '../NFTitem/NFTitem'; + +export interface RowData { + id: number; + name: string; + age: number; + email: string; + status: string; +} + +export const data: RowData[] = [ + { + id: 1, + name: 'John Doe', + age: 28, + email: 'john.doe@example.com', + status: 'Active', + }, + { + id: 2, + name: 'Jane Smith', + age: 34, + email: 'jane.smith@example.com', + status: 'Inactive', + }, + { + id: 3, + name: 'Michael Johnson', + age: 45, + email: 'michael.johnson@example.com', + status: 'Active', + }, + { + id: 4, + name: 'Emily Davis', + age: 23, + email: 'emily.davis@example.com', + status: 'Pending', + }, + { + id: 5, + name: 'William Brown', + age: 39, + email: 'william.brown@example.com', + status: 'Active', + }, + { + id: 6, + name: 'Olivia Taylor', + age: 29, + email: 'olivia.taylor@example.com', + status: 'Inactive', + }, + { + id: 7, + name: 'James Anderson', + age: 32, + email: 'james.anderson@example.com', + status: 'Pending', + }, + { + id: 8, + name: 'Sophia Thomas', + age: 27, + email: 'sophia.thomas@example.com', + status: 'Active', + }, + { + id: 9, + name: 'Isabella Lee', + age: 31, + email: 'isabella.lee@example.com', + status: 'Inactive', + }, + { + id: 10, + name: 'David Martinez', + age: 36, + email: 'david.martinez@example.com', + status: 'Active', + }, +]; + +export const columns = [ + { + name: 'Rank', + cell: () =>
1
, + sortable: true, + }, + { + name: 'Account Adderess', + cell: () => ( +
+ +
+ ), + sortable: true, + }, + { + name: 'Balance', + cell: () => ( +
+ +
+ ), + sortable: true, + }, + { + name: 'Percentage', + cell: () =>
22.00%
, + sortable: false, + }, + { + name: 'Tx Count', + cell: () => ( +
+ +
+ ), + sortable: true, + }, +]; + +export function TxsAccountTable() { + const [currentGridPage, setCurrentGridPage] = useState(0); + return ( +
+ {}} + pageCount={2} + currentPage={currentGridPage} + setCurrentPage={setCurrentGridPage} + /> +
+ ); +} diff --git a/packages/app-explorer/src/systems/Transactions/components/TxsButton/TxsButton.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsButton/TxsButton.tsx new file mode 100644 index 000000000..4fdc34a5f --- /dev/null +++ b/packages/app-explorer/src/systems/Transactions/components/TxsButton/TxsButton.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +interface TxsButtonProps { + children: React.ReactNode; // Use children instead of buttonName + type?: 'button' | 'submit' | 'reset'; +} + +function TxsButton({ children }: TxsButtonProps) { + return ( +
+ {children} +
+ ); +} + +export default TxsButton; diff --git a/packages/app-explorer/src/systems/Transactions/components/TxsButtonContainer/TxsButtonContainer.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsButtonContainer/TxsButtonContainer.tsx new file mode 100644 index 000000000..301362057 --- /dev/null +++ b/packages/app-explorer/src/systems/Transactions/components/TxsButtonContainer/TxsButtonContainer.tsx @@ -0,0 +1,65 @@ +import React, { useState } from 'react'; +import ModalContainer from '../ModalContainer/ModalContainer'; +import TxsButton from '../TxsButton/TxsButton'; +import TxsExportCsvModal from '../TxsExportCsvModal/TxsExportCsvModal'; + +function TxsButtonContainer() { + const [isModalOpen, setIsModalOpen] = useState(false); + + const openModal = () => setIsModalOpen(true); + const closeModal = () => setIsModalOpen(false); + + return ( +
+ +

Download Page Data

+ + + +
+ + + + + + + + + +
+ ); +} + +export default TxsButtonContainer; diff --git a/packages/app-explorer/src/systems/Transactions/components/TxsExportCsvModal/TxsExportCsvModal.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsExportCsvModal/TxsExportCsvModal.tsx new file mode 100644 index 000000000..bb810ed67 --- /dev/null +++ b/packages/app-explorer/src/systems/Transactions/components/TxsExportCsvModal/TxsExportCsvModal.tsx @@ -0,0 +1,139 @@ +'use client'; +import { Select } from '@fuels/ui'; +import React, { useState } from 'react'; + +type TxsExportCsvModalProps = { + closeModal: () => void; +}; + +const TxsExportCsvModal: React.FC = ({ + closeModal, +}) => { + const [_transactionType, _setTransactionType] = + useState('Transactions'); + const [address, setAddress] = useState(''); + const [downloadOption, setDownloadOption] = useState<'date' | 'dataBlock'>( + 'date', + ); + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); + + const handleExportData = (): void => {}; + + return ( +
+
+

Export CSV

+ +
+ {/*
+ + +
*/} + + +
+ + setAddress(e.target.value)} + /> +
+
+ +
+ + +
+
+
+
+ + setStartDate(e.target.value)} + /> +
+
+ + setEndDate(e.target.value)} + /> +
+
+ +

+ The earliest 5,000 records within the selected range will be exported +

+
+ ); +}; + +export default TxsExportCsvModal; diff --git a/packages/app-explorer/src/systems/Transactions/components/TxsTab/TsxTab.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsTab/TsxTab.tsx new file mode 100644 index 000000000..cf1229b24 --- /dev/null +++ b/packages/app-explorer/src/systems/Transactions/components/TxsTab/TsxTab.tsx @@ -0,0 +1,30 @@ +'use client'; +import React from 'react'; + +interface TxsTabProps { + tabTitle: string; + isActive: boolean; + onClick: () => void; // onClick prop +} + +export function TxsTab({ tabTitle, isActive, onClick }: TxsTabProps) { + // Handle key events + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + // Allow activation with Enter or Space + onClick(); + } + }; + + return ( +

+ {tabTitle} +

+ ); +} diff --git a/packages/app-explorer/src/systems/Transactions/components/TxsTabList/TxsTabList.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsTabList/TxsTabList.tsx new file mode 100644 index 000000000..69c223cf5 --- /dev/null +++ b/packages/app-explorer/src/systems/Transactions/components/TxsTabList/TxsTabList.tsx @@ -0,0 +1,34 @@ +'use client'; +import { TxsTab } from '../TxsTab/TsxTab'; + +export type TxsTabListProps = { + activeTab: string; + setActiveTab: (tabName: string) => void; +}; + +export function TxsTabList({ activeTab, setActiveTab }: TxsTabListProps) { + // Default active tab + + const tabList = [ + { name: 'Top Tokens' }, + { name: 'Top NFTs' }, + { name: 'Top Accounts' }, + ]; + + const handleTabClick = (tabName: string) => { + setActiveTab(tabName); + }; + + return ( +
+ {tabList.map((tab) => ( + handleTabClick(tab.name)} + /> + ))} +
+ ); +} diff --git a/packages/app-explorer/src/systems/Transactions/components/TxsTitle/TxsTitle.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsTitle/TxsTitle.tsx index eaefa40c2..c5e65e2ab 100644 --- a/packages/app-explorer/src/systems/Transactions/components/TxsTitle/TxsTitle.tsx +++ b/packages/app-explorer/src/systems/Transactions/components/TxsTitle/TxsTitle.tsx @@ -1,4 +1,31 @@ -'use client'; +// 'use client'; +// import { PageTitle } from 'app-commons'; +// import TxsButtonContainer from '../TxsButtonContainer/TxsButtonContainer'; +// import { TxsTabList } from '../TxsTabList/TxsTabList'; +// // import TxsExportCsvModal from '../TxsExportCsvModal/TxsExportCsvModal'; + +// export type TxsTabListProps = { +// activeTab: string; +// setActiveTab: (tabName: string) => void; +// }; + +// export function TxsTitle({ activeTab='Recent Transactions', setActiveTab=()=>void }: TxsTabListProps) { +// return ( +// <> +// } +// > +// Top Tokens +// +// +// +// {/* */} +// +// ); +// } + import { PageTitle } from 'app-commons'; export function TxsTitle() { diff --git a/packages/app-explorer/src/systems/Transactions/components/TxsTokenTable/TxsTokenTable.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsTokenTable/TxsTokenTable.tsx new file mode 100644 index 000000000..eedcc8aac --- /dev/null +++ b/packages/app-explorer/src/systems/Transactions/components/TxsTokenTable/TxsTokenTable.tsx @@ -0,0 +1,157 @@ +import { GridTable } from '@fuels/ui'; +import { useState } from 'react'; +import NFTHashItem from '../NFTHashItem/NFTHashItem'; +import NFTProfileHolderIcon from '../NFTProfileHolderIcon/NFTProfileHolderIcon'; +import NFTItem from '../NFTitem/NFTitem'; + +export interface RowData { + id: number; + name: string; + age: number; + email: string; + status: string; +} + +export const data: RowData[] = [ + { + id: 1, + name: 'John Doe', + age: 28, + email: 'john.doe@example.com', + status: 'Active', + }, + { + id: 2, + name: 'Jane Smith', + age: 34, + email: 'jane.smith@example.com', + status: 'Inactive', + }, + { + id: 3, + name: 'Michael Johnson', + age: 45, + email: 'michael.johnson@example.com', + status: 'Active', + }, + { + id: 4, + name: 'Emily Davis', + age: 23, + email: 'emily.davis@example.com', + status: 'Pending', + }, + { + id: 5, + name: 'William Brown', + age: 39, + email: 'william.brown@example.com', + status: 'Active', + }, + { + id: 6, + name: 'Olivia Taylor', + age: 29, + email: 'olivia.taylor@example.com', + status: 'Inactive', + }, + { + id: 7, + name: 'James Anderson', + age: 32, + email: 'james.anderson@example.com', + status: 'Pending', + }, + { + id: 8, + name: 'Sophia Thomas', + age: 27, + email: 'sophia.thomas@example.com', + status: 'Active', + }, + { + id: 9, + name: 'Isabella Lee', + age: 31, + email: 'isabella.lee@example.com', + status: 'Inactive', + }, + { + id: 10, + name: 'David Martinez', + age: 36, + email: 'david.martinez@example.com', + status: 'Active', + }, +]; + +export const columns = [ + { + name: 'Rank', + cell: () =>
1
, + sortable: true, + }, + { + name: 'NFT Collection', + cell: () => ( +
+ +
+ ), + sortable: true, + }, + { + name: 'Mint Adderess', + cell: () => ( +
+ +
+ ), + sortable: true, + }, + { + name: 'Floor Price', + cell: () =>
22.00%
, + sortable: false, + }, + { + name: 'Total Supply', + cell: () => ( +
+ +
+ ), + sortable: true, + }, + { + name: 'Holders', + cell: () => ( +
+ +
+ ), + sortable: true, + }, +]; + +export function TxsTokenTable() { + const [currentGridPage, setCurrentGridPage] = useState(0); + return ( +
+ {}} + pageCount={2} + currentPage={currentGridPage} + setCurrentPage={setCurrentGridPage} + /> +
+ ); +} diff --git a/packages/app-explorer/src/systems/utils/date.ts b/packages/app-explorer/src/systems/utils/date.ts new file mode 100644 index 000000000..9995818df --- /dev/null +++ b/packages/app-explorer/src/systems/utils/date.ts @@ -0,0 +1,16 @@ +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import { TAI64 } from 'tai64'; + +dayjs.extend(relativeTime); + +export class DateHelper { + static tai64toDate(tai64Timestamp: string) { + const timestamp = TAI64.fromString(tai64Timestamp, 10).toUnix(); + return dayjs(timestamp * 1000); + } + + static dateToTai64(date: Date) { + return TAI64.fromUnix(Math.floor(date.getTime() / 1000)).toString(10); + } +} diff --git a/packages/app-explorer/src/systems/utils/utils.ts b/packages/app-explorer/src/systems/utils/utils.ts new file mode 100644 index 000000000..f8e579de3 --- /dev/null +++ b/packages/app-explorer/src/systems/utils/utils.ts @@ -0,0 +1,204 @@ +import { DateHelper } from './date'; + +interface TransactionNode { + __typename: 'TransactionFee'; + fee: string; + timestamp: string; +} + +interface AccountNode { + timestamp: string; + count?: number; +} + +interface Interval { + start: Date; + end: Date; + count: number; // To track the number of transactions + totalFee: number; // To track the total transaction fees +} + +export function getUnitAndInterval(timeRange: string): { + unit: 'minute' | 'hour' | 'day' | 'month'; + intervalSize: number; +} { + switch (timeRange) { + case '1hr': + return { unit: 'minute', intervalSize: 5 }; + case '12hr': + return { unit: 'hour', intervalSize: 1 }; + case '1day': + return { unit: 'hour', intervalSize: 2 }; + case '7days': + return { unit: 'hour', intervalSize: 12 }; + case '14days': + return { unit: 'day', intervalSize: 1 }; + case '30days': + return { unit: 'day', intervalSize: 3 }; + case '90days': + return { unit: 'day', intervalSize: 10 }; + default: + return { unit: 'month', intervalSize: 1 }; + } +} + +function roundToNearest( + time: number, + unit: 'minute' | 'hour' | 'day' | 'month', + roundUp = false, +): number { + const date = new Date(time); + + switch (unit) { + case 'minute': { + const msInMinute = 60 * 1000; + const msInFiveMinutes = 5 * msInMinute; + return roundUp + ? Math.ceil(time / msInFiveMinutes) * msInFiveMinutes + : Math.floor(time / msInFiveMinutes) * msInFiveMinutes; + } + + case 'hour': { + const msInHour = 60 * 60 * 1000; + return roundUp + ? Math.ceil(time / msInHour) * msInHour + : Math.floor(time / msInHour) * msInHour; + } + + case 'day': + if (roundUp) { + date.setUTCHours(0, 0, 0, 0); + return date.getTime() + 24 * 60 * 60 * 1000; // Add one day + } + date.setUTCHours(0, 0, 0, 0); // Set to midnight + return date.getTime(); + + case 'month': + if (roundUp) { + if (date.getUTCMonth() === 11) { + // If December, increment year + date.setUTCFullYear(date.getUTCFullYear() + 1); + date.setUTCMonth(0); + } else { + date.setUTCMonth(date.getUTCMonth() + 1); + } + date.setUTCDate(1); // First day of the next month + } else { + date.setUTCDate(1); // First day of the current month + } + date.setUTCHours(0, 0, 0, 0); // Set time to midnight + return date.getTime(); + } +} + +// General interval creation function +export function createIntervals( + startTime: number, + endTime: number, + unit: 'minute' | 'hour' | 'day' | 'month', + intervalSize: number, +): Array<{ start: Date; end: Date; count: number; totalFee: number }> { + const roundedStartTime = roundToNearest(startTime, unit); + const roundedEndTime = roundToNearest(endTime, unit, true); + + const intervals: Array<{ + start: Date; + end: Date; + count: number; + totalFee: number; + }> = []; + + let currentTime = roundedStartTime; + + if (unit === 'month') { + // Handle month-specific interval logic (varying days in a month) + const currentDate = new Date(roundedStartTime); + while (currentDate.getTime() < roundedEndTime) { + const startInterval = new Date(currentDate); + currentDate.setUTCMonth(currentDate.getUTCMonth() + intervalSize); // Move by `intervalSize` months + const endInterval = new Date(currentDate); + intervals.push({ + start: startInterval, + end: endInterval, + count: 0, + totalFee: 0, + }); + } + } else { + // Handle minute, hour, and day intervals + const msInUnit = { + minute: 60 * 1000, + hour: 60 * 60 * 1000, + day: 24 * 60 * 60 * 1000, + }; + + const intervalDuration = intervalSize * msInUnit[unit]; + + while (currentTime < roundedEndTime) { + const startInterval = new Date(currentTime); + const endInterval = new Date(currentTime + intervalDuration); + intervals.push({ + start: startInterval, + end: endInterval, + count: 0, + totalFee: 0, + }); + currentTime += intervalDuration; + } + } + + return intervals; +} + +// Helper to process transactions and map to intervals +export function processTransactions( + nodes: TransactionNode[], + intervalMap: Interval[], +) { + nodes.forEach((transaction) => { + const transactionTimestamp = Number( + DateHelper.tai64toDate(transaction.timestamp), + ); + const transactionFee = Number(transaction.fee); + + // Find the correct interval for the current transaction + for (const interval of intervalMap) { + const intervalStart = new Date(interval.start).getTime(); + const intervalEnd = new Date(interval.end).getTime(); + + if ( + transactionTimestamp >= intervalStart && + transactionTimestamp < intervalEnd + ) { + // Increment count and add the transaction fee to totalFee + interval.count += 1; + interval.totalFee += transactionFee; + break; // Transaction assigned, no need to check further + } + } + }); + return intervalMap; +} + +export function processAccounts(nodes: AccountNode[], intervalMap: Interval[]) { + nodes.forEach((account) => { + const accountTimestamp = Number(account.timestamp); + + // Find the correct interval for the current account + for (const interval of intervalMap) { + const intervalStart = new Date(interval.start).getTime(); + const intervalEnd = new Date(interval.end).getTime(); + + if (accountTimestamp >= intervalStart && accountTimestamp < intervalEnd) { + // Increment count for the number of accounts created + if (account.count) { + interval.count += account.count; + } else { + interval.count += 1; + } + break; // Account assigned, no need to check further + } + } + }); + return intervalMap; +} diff --git a/packages/app-portal/src/systems/Chains/eth/hooks/useAddAssetForm.ts b/packages/app-portal/src/systems/Chains/eth/hooks/useAddAssetForm.ts index d30fe0ec3..ee789ca53 100644 --- a/packages/app-portal/src/systems/Chains/eth/hooks/useAddAssetForm.ts +++ b/packages/app-portal/src/systems/Chains/eth/hooks/useAddAssetForm.ts @@ -17,7 +17,7 @@ type UseAddAssetOpts = { }; export const useAddAssetForm = (opts: UseAddAssetOpts = {}) => { - const schema = yup.object({ + const schema: any = yup.object({ symbol: yup.string().required('Symbol is required'), decimals: yup .string() diff --git a/packages/app-portal/src/systems/Chains/eth/hooks/useSetAssetAddressForm.ts b/packages/app-portal/src/systems/Chains/eth/hooks/useSetAssetAddressForm.ts index f91804ced..a1e0c11a7 100644 --- a/packages/app-portal/src/systems/Chains/eth/hooks/useSetAssetAddressForm.ts +++ b/packages/app-portal/src/systems/Chains/eth/hooks/useSetAssetAddressForm.ts @@ -15,7 +15,7 @@ type UseSetAddressOpts = { }; export const useSetAddressForm = (opts: UseSetAddressOpts = {}) => { - const schema = yup.object({ + const schema: any = yup.object({ address: yup.string().required('Address is required'), }); diff --git a/packages/graphql/.gitignore b/packages/graphql/.gitignore index e69de29bb..941c2c569 100644 --- a/packages/graphql/.gitignore +++ b/packages/graphql/.gitignore @@ -0,0 +1,2 @@ +logs +check.sh \ No newline at end of file diff --git a/packages/graphql/database/1.sql b/packages/graphql/database/1.sql new file mode 100644 index 000000000..0729e1cfc --- /dev/null +++ b/packages/graphql/database/1.sql @@ -0,0 +1,14 @@ +CREATE TABLE indexer.accounts ( + _id SERIAL PRIMARY KEY, + account_id character varying(66) NOT NULL UNIQUE, + balance BIGINT NOT NULL DEFAULT 0, + transaction_count INTEGER NOT NULL DEFAULT 0, + data jsonb NOT NULL DEFAULT '{}', + first_transaction_timestamp timestamp without time zone NOT NULL, + recent_transaction_timestamp timestamp without time zone NOT NULL +); +CREATE UNIQUE INDEX ON indexer.accounts(_id); +CREATE UNIQUE INDEX ON indexer.accounts(account_id); +CREATE INDEX ON indexer.accounts(transaction_count); +CREATE INDEX ON indexer.accounts(recent_transaction_timestamp); +CREATE INDEX ON indexer.accounts(first_transaction_timestamp); \ No newline at end of file diff --git a/packages/graphql/package.json b/packages/graphql/package.json index ff304c2bd..d06244ff1 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -14,7 +14,9 @@ }, "typesVersions": { "*": { - "sdk": ["./src/graphql/generated/sdk.ts"] + "sdk": [ + "./src/graphql/generated/sdk.ts" + ] } }, "scripts": { @@ -70,6 +72,7 @@ "pino": "9.2.0", "pino-pretty": "11.2.1", "pm2": "^5.4.1", + "recharts": "2.12.7", "shallow-equal-object": "1.1.1", "tai64": "^1.0.0", "winston": "3.13.0", diff --git a/packages/graphql/src/db/migrations/createBlock.ts b/packages/graphql/src/db/migrations/createBlock.ts new file mode 100644 index 000000000..b6bf9dd58 --- /dev/null +++ b/packages/graphql/src/db/migrations/createBlock.ts @@ -0,0 +1,210 @@ +import { DatabaseConnection } from '../../infra/database/DatabaseConnection'; + +async function migrate() { + const db = DatabaseConnection.getInstance(); + + // Step 1: Drop the existing indexer schema if it exists + await db.query( + ` + DROP SCHEMA IF EXISTS indexer CASCADE; + `, + [], // Provide an empty array for query parameters + ); + console.log('Existing indexer schema dropped if it was present.'); + + // Step 2: Create the new indexer schema + await db.query( + ` + CREATE SCHEMA indexer; + `, + [], + ); + console.log('New indexer schema created successfully.'); + + // Step 3: Create the blocks table + await db.query( + ` + CREATE TABLE indexer.blocks ( + _id INTEGER PRIMARY KEY, + id CHARACTER VARYING(66) NOT NULL UNIQUE, + timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, + data JSONB NOT NULL, + gas_used CHARACTER VARYING(66), + producer CHARACTER VARYING(66) + ); + `, + [], + ); + console.log('Blocks table created successfully.'); + + // Step 4: Create indexes for blocks table + await db.query( + ` + CREATE UNIQUE INDEX ON indexer.blocks(_id); + CREATE UNIQUE INDEX ON indexer.blocks(id); + CREATE INDEX ON indexer.blocks(timestamp); + CREATE INDEX ON indexer.blocks(id); + CREATE INDEX ON indexer.blocks(_id); + `, + [], + ); + console.log('Indexes for blocks table created successfully.'); + + // Step 5: Create the transactions table + await db.query( + ` + CREATE TABLE indexer.transactions ( + _id CHARACTER VARYING(66) PRIMARY KEY, + tx_hash CHARACTER VARYING(66) NOT NULL UNIQUE, + timestamp TIMESTAMP WITHOUT TIME ZONE, + data JSONB NOT NULL, + block_id INTEGER NOT NULL REFERENCES indexer.blocks(_id) + ); + `, + [], + ); + console.log('Transactions table created successfully.'); + + // Step 6: Create indexes for transactions table + await db.query( + ` + CREATE UNIQUE INDEX ON indexer.transactions(_id); + CREATE UNIQUE INDEX ON indexer.transactions(tx_hash); + CREATE INDEX ON indexer.transactions(timestamp); + CREATE INDEX ON indexer.transactions(_id); + CREATE INDEX ON indexer.transactions(block_id); + CREATE INDEX ON indexer.transactions(tx_hash); + `, + [], + ); + console.log('Indexes for transactions table created successfully.'); + + // Step 7: Create the contracts table + await db.query( + ` + CREATE TABLE indexer.contracts ( + _id SERIAL PRIMARY KEY, + contract_hash CHARACTER VARYING(66) NOT NULL UNIQUE, + data JSONB NOT NULL + ); + `, + [], + ); + console.log('Contracts table created successfully.'); + + // Step 8: Create indexes for contracts table + await db.query( + ` + CREATE UNIQUE INDEX ON indexer.contracts(_id); + CREATE UNIQUE INDEX ON indexer.contracts(contract_hash); + CREATE INDEX ON indexer.contracts(_id); + CREATE INDEX ON indexer.contracts(contract_hash); + `, + [], + ); + console.log('Indexes for contracts table created successfully.'); + + // Step 9: Create the inputs table + await db.query( + ` + CREATE TABLE indexer.inputs ( + _id SERIAL PRIMARY KEY, + data JSONB NOT NULL, + transaction_id CHARACTER VARYING(66) NOT NULL REFERENCES indexer.transactions(_id) + ); + `, + [], + ); + console.log('Inputs table created successfully.'); + + // Step 10: Create indexes for inputs table + await db.query( + ` + CREATE UNIQUE INDEX ON indexer.inputs(_id); + CREATE INDEX ON indexer.inputs(_id); + CREATE INDEX ON indexer.inputs(transaction_id); + `, + [], + ); + console.log('Indexes for inputs table created successfully.'); + + // Step 11: Create the outputs table + await db.query( + ` + CREATE TABLE indexer.outputs ( + _id SERIAL PRIMARY KEY, + data JSONB NOT NULL, + transaction_id CHARACTER VARYING(66) NOT NULL REFERENCES indexer.transactions(_id) + ); + `, + [], + ); + console.log('Outputs table created successfully.'); + + // Step 12: Create indexes for outputs table + await db.query( + ` + CREATE UNIQUE INDEX ON indexer.outputs(_id); + CREATE INDEX ON indexer.outputs(_id); + CREATE INDEX ON indexer.outputs(transaction_id); + `, + [], + ); + console.log('Indexes for outputs table created successfully.'); + + // Step 13: Create the predicates table + await db.query( + ` + CREATE TABLE indexer.predicates ( + _id SERIAL PRIMARY KEY, + bytecode TEXT NOT NULL, + address CHARACTER VARYING(66) NOT NULL UNIQUE + ); + `, + [], + ); + console.log('Predicates table created successfully.'); + + // Step 14: Create indexes for predicates table + await db.query( + ` + CREATE UNIQUE INDEX ON indexer.predicates(_id); + CREATE UNIQUE INDEX ON indexer.predicates(address); + CREATE INDEX ON indexer.predicates(_id); + CREATE INDEX ON indexer.predicates(address); + `, + [], + ); + console.log('Indexes for predicates table created successfully.'); + + // Step 15: Create the transactions_accounts table + await db.query( + ` + CREATE TABLE indexer.transactions_accounts ( + _id TEXT NOT NULL, + block_id INTEGER NOT NULL, + tx_hash TEXT NOT NULL, + account_hash TEXT NOT NULL, + PRIMARY KEY (_id, block_id, tx_hash, account_hash) + ); + `, + [], + ); + console.log('Transactions_Accounts table created successfully.'); + + // Step 16: Create indexes for transactions_accounts table + await db.query( + ` + CREATE INDEX ON indexer.transactions_accounts (_id); + CREATE INDEX ON indexer.transactions_accounts (block_id); + CREATE INDEX ON indexer.transactions_accounts (tx_hash); + CREATE INDEX ON indexer.transactions_accounts (account_hash); + `, + [], + ); + console.log('Indexes for transactions_accounts table created successfully.'); +} + +migrate() + .catch(console.error) + .finally(() => process.exit()); diff --git a/packages/graphql/src/domain/Account/AccountEntity.ts b/packages/graphql/src/domain/Account/AccountEntity.ts new file mode 100644 index 000000000..4c58d7136 --- /dev/null +++ b/packages/graphql/src/domain/Account/AccountEntity.ts @@ -0,0 +1,73 @@ +import { Hash256 } from '../../application/vo/Hash256'; +import { Entity } from '../../core/Entity'; +import { AccountBalance } from './vo/AccountBalance'; +import { AccountData } from './vo/AccountData'; +import { AccountModelID } from './vo/AccountModelID'; + +type AccountInputProps = { + account_id: Hash256; + balance: AccountBalance; + data: AccountData; + transactionCount: number; +}; + +export class AccountEntity extends Entity { + // Adjust the constructor to not require an ID initially + static create(account: any) { + const account_id = Hash256.create(account.account_id); + const balance = AccountBalance.create(account.balance); + const data = AccountData.create(account.data); + const transactionCount = account.transactionCount || 0; + + const props: AccountInputProps = { + account_id, + balance, + data, + transactionCount, + }; + + // If _id is not provided, set it as undefined + const id = account._id ? AccountModelID.create(account._id) : undefined; + + return new AccountEntity(props, id); + } + + static toDBItem(account: AccountEntity): any { + return { + account_id: account.props.account_id.value(), + balance: account.props.balance.value().toString(), + data: AccountEntity.serializeData(account.props.data.value()), + transaction_count: account.props.transactionCount, + }; + } + + static serializeData(data: any): string { + return JSON.stringify(data, (_, value) => + typeof value === 'bigint' ? value.toString() : value, + ); + } + + get cursor() { + return this.id ? this.id.value() : null; + } + + get id() { + return this._id; + } + + get account_id() { + return this.props.account_id.value(); + } + + get balance() { + return this.props.balance.value(); + } + + get transactionCount() { + return this.props.transactionCount; + } + + get data() { + return this.props.data.value(); + } +} diff --git a/packages/graphql/src/domain/Account/vo/AccountBalance.ts b/packages/graphql/src/domain/Account/vo/AccountBalance.ts new file mode 100644 index 000000000..be538e838 --- /dev/null +++ b/packages/graphql/src/domain/Account/vo/AccountBalance.ts @@ -0,0 +1,27 @@ +import { bigint as DrizzleBigint } from 'drizzle-orm/pg-core'; +import { ValueObject } from '../../../core/ValueObject'; +interface Props { + value: bigint; +} + +export class AccountBalance extends ValueObject { + private constructor(props: Props) { + super(props); + } + + static type() { + return DrizzleBigint('balance', { mode: 'bigint' }).notNull(); + } + + static create(value: bigint) { + return new AccountBalance({ value }); + } + + value() { + return this.props.value; + } + + add(amount: bigint): AccountBalance { + return new AccountBalance({ value: this.value() + amount }); + } +} diff --git a/packages/graphql/src/domain/Account/vo/AccountData.ts b/packages/graphql/src/domain/Account/vo/AccountData.ts new file mode 100644 index 000000000..080d74b73 --- /dev/null +++ b/packages/graphql/src/domain/Account/vo/AccountData.ts @@ -0,0 +1,24 @@ +import { jsonb } from 'drizzle-orm/pg-core'; +import { ValueObject } from '../../../core/ValueObject'; + +interface Props { + value: any; +} + +export class AccountData extends ValueObject { + private constructor(props: Props) { + super(props); + } + + static type() { + return jsonb('data').notNull(); + } + + static create(value: any) { + return new AccountData({ value }); + } + + value() { + return this.props.value; + } +} diff --git a/packages/graphql/src/domain/Account/vo/AccountModelID.ts b/packages/graphql/src/domain/Account/vo/AccountModelID.ts new file mode 100644 index 000000000..e5d6513f4 --- /dev/null +++ b/packages/graphql/src/domain/Account/vo/AccountModelID.ts @@ -0,0 +1,20 @@ +import { integer } from 'drizzle-orm/pg-core'; +import { Identifier } from '../../../core/Identifier'; + +export class AccountModelID extends Identifier { + private constructor(id: number) { + super(id); + } + + static type() { + return integer('_id').primaryKey(); + } + + static create(id: number): AccountModelID { + if (typeof id !== 'number' || Number.isNaN(id)) { + throw new Error('Invalid ID: ID must be a valid number.'); + } + + return new AccountModelID(id); + } +} diff --git a/packages/graphql/src/domain/Account/vo/AccountRef.ts b/packages/graphql/src/domain/Account/vo/AccountRef.ts new file mode 100644 index 000000000..ca4f4474e --- /dev/null +++ b/packages/graphql/src/domain/Account/vo/AccountRef.ts @@ -0,0 +1,18 @@ +import { ValueObject } from '../../../core/ValueObject'; +interface Props { + value: number; +} + +export class AccountRef extends ValueObject { + private constructor(props: Props) { + super(props); + } + + static create(id: number) { + return new AccountRef({ value: id }); + } + + value() { + return this.props.value; + } +} diff --git a/packages/graphql/src/graphql/generated/fuelcore/queries/cumulativeTransactionsFeeStatistics.graphql b/packages/graphql/src/graphql/generated/fuelcore/queries/cumulativeTransactionsFeeStatistics.graphql new file mode 100644 index 000000000..0e3c5bcb1 --- /dev/null +++ b/packages/graphql/src/graphql/generated/fuelcore/queries/cumulativeTransactionsFeeStatistics.graphql @@ -0,0 +1,9 @@ +query cumulativeTransactionsFeeStatistics($timeFilter: String){ + cumulativeTransactionsFeeStatistics(timeFilter: $timeFilter){ + nodes{ + fee + timestamp + } + transactionOffset + } +} \ No newline at end of file diff --git a/packages/graphql/src/graphql/generated/fuelcore/queries/getBlocksDashboard.graphql b/packages/graphql/src/graphql/generated/fuelcore/queries/getBlocksDashboard.graphql new file mode 100644 index 000000000..e6f943f4d --- /dev/null +++ b/packages/graphql/src/graphql/generated/fuelcore/queries/getBlocksDashboard.graphql @@ -0,0 +1,10 @@ +query getBlocksDashboard{ + getBlocksDashboard{ + nodes{ + timestamp + gasUsed + blockNo + producer + } + } +} \ No newline at end of file diff --git a/packages/graphql/src/graphql/generated/fuelcore/queries/index.js b/packages/graphql/src/graphql/generated/fuelcore/queries/index.js index f99a6bf08..eae1a0017 100644 --- a/packages/graphql/src/graphql/generated/fuelcore/queries/index.js +++ b/packages/graphql/src/graphql/generated/fuelcore/queries/index.js @@ -1,30 +1,119 @@ const fs = require('fs'); const path = require('path'); -module.exports.balance = fs.readFileSync(path.join(__dirname, 'balance.graphql'), 'utf8'); -module.exports.balances = fs.readFileSync(path.join(__dirname, 'balances.graphql'), 'utf8'); -module.exports.block = fs.readFileSync(path.join(__dirname, 'block.graphql'), 'utf8'); -module.exports.blocks = fs.readFileSync(path.join(__dirname, 'blocks.graphql'), 'utf8'); -module.exports.chain = fs.readFileSync(path.join(__dirname, 'chain.graphql'), 'utf8'); -module.exports.coin = fs.readFileSync(path.join(__dirname, 'coin.graphql'), 'utf8'); -module.exports.coins = fs.readFileSync(path.join(__dirname, 'coins.graphql'), 'utf8'); -module.exports.coinsToSpend = fs.readFileSync(path.join(__dirname, 'coinsToSpend.graphql'), 'utf8'); -module.exports.contract = fs.readFileSync(path.join(__dirname, 'contract.graphql'), 'utf8'); -module.exports.contractBalance = fs.readFileSync(path.join(__dirname, 'contractBalance.graphql'), 'utf8'); -module.exports.contractBalances = fs.readFileSync(path.join(__dirname, 'contractBalances.graphql'), 'utf8'); -module.exports.estimateGasPrice = fs.readFileSync(path.join(__dirname, 'estimateGasPrice.graphql'), 'utf8'); -module.exports.estimatePredicates = fs.readFileSync(path.join(__dirname, 'estimatePredicates.graphql'), 'utf8'); -module.exports.health = fs.readFileSync(path.join(__dirname, 'health.graphql'), 'utf8'); -module.exports.latestGasPrice = fs.readFileSync(path.join(__dirname, 'latestGasPrice.graphql'), 'utf8'); -module.exports.memory = fs.readFileSync(path.join(__dirname, 'memory.graphql'), 'utf8'); -module.exports.message = fs.readFileSync(path.join(__dirname, 'message.graphql'), 'utf8'); -module.exports.messageProof = fs.readFileSync(path.join(__dirname, 'messageProof.graphql'), 'utf8'); -module.exports.messageStatus = fs.readFileSync(path.join(__dirname, 'messageStatus.graphql'), 'utf8'); -module.exports.messages = fs.readFileSync(path.join(__dirname, 'messages.graphql'), 'utf8'); -module.exports.nodeInfo = fs.readFileSync(path.join(__dirname, 'nodeInfo.graphql'), 'utf8'); -module.exports.register = fs.readFileSync(path.join(__dirname, 'register.graphql'), 'utf8'); -module.exports.relayedTransactionStatus = fs.readFileSync(path.join(__dirname, 'relayedTransactionStatus.graphql'), 'utf8'); -module.exports.transaction = fs.readFileSync(path.join(__dirname, 'transaction.graphql'), 'utf8'); -module.exports.transactions = fs.readFileSync(path.join(__dirname, 'transactions.graphql'), 'utf8'); -module.exports.transactionsByOwner = fs.readFileSync(path.join(__dirname, 'transactionsByOwner.graphql'), 'utf8'); -module.exports.transactionsByBlockId = fs.readFileSync(path.join(__dirname, 'transactionsByBlockId.graphql'), 'utf8'); +module.exports.balance = fs.readFileSync( + path.join(__dirname, 'balance.graphql'), + 'utf8', +); +module.exports.balances = fs.readFileSync( + path.join(__dirname, 'balances.graphql'), + 'utf8', +); +module.exports.block = fs.readFileSync( + path.join(__dirname, 'block.graphql'), + 'utf8', +); +module.exports.blocks = fs.readFileSync( + path.join(__dirname, 'blocks.graphql'), + 'utf8', +); +module.exports.chain = fs.readFileSync( + path.join(__dirname, 'chain.graphql'), + 'utf8', +); +module.exports.coin = fs.readFileSync( + path.join(__dirname, 'coin.graphql'), + 'utf8', +); +module.exports.coins = fs.readFileSync( + path.join(__dirname, 'coins.graphql'), + 'utf8', +); +module.exports.coinsToSpend = fs.readFileSync( + path.join(__dirname, 'coinsToSpend.graphql'), + 'utf8', +); +module.exports.contract = fs.readFileSync( + path.join(__dirname, 'contract.graphql'), + 'utf8', +); +module.exports.contractBalance = fs.readFileSync( + path.join(__dirname, 'contractBalance.graphql'), + 'utf8', +); +module.exports.contractBalances = fs.readFileSync( + path.join(__dirname, 'contractBalances.graphql'), + 'utf8', +); +module.exports.estimateGasPrice = fs.readFileSync( + path.join(__dirname, 'estimateGasPrice.graphql'), + 'utf8', +); +module.exports.estimatePredicates = fs.readFileSync( + path.join(__dirname, 'estimatePredicates.graphql'), + 'utf8', +); +module.exports.health = fs.readFileSync( + path.join(__dirname, 'health.graphql'), + 'utf8', +); +module.exports.latestGasPrice = fs.readFileSync( + path.join(__dirname, 'latestGasPrice.graphql'), + 'utf8', +); +module.exports.memory = fs.readFileSync( + path.join(__dirname, 'memory.graphql'), + 'utf8', +); +module.exports.message = fs.readFileSync( + path.join(__dirname, 'message.graphql'), + 'utf8', +); +module.exports.messageProof = fs.readFileSync( + path.join(__dirname, 'messageProof.graphql'), + 'utf8', +); +module.exports.messageStatus = fs.readFileSync( + path.join(__dirname, 'messageStatus.graphql'), + 'utf8', +); +module.exports.messages = fs.readFileSync( + path.join(__dirname, 'messages.graphql'), + 'utf8', +); +module.exports.nodeInfo = fs.readFileSync( + path.join(__dirname, 'nodeInfo.graphql'), + 'utf8', +); +module.exports.register = fs.readFileSync( + path.join(__dirname, 'register.graphql'), + 'utf8', +); +module.exports.relayedTransactionStatus = fs.readFileSync( + path.join(__dirname, 'relayedTransactionStatus.graphql'), + 'utf8', +); +module.exports.transaction = fs.readFileSync( + path.join(__dirname, 'transaction.graphql'), + 'utf8', +); +module.exports.transactions = fs.readFileSync( + path.join(__dirname, 'transactions.graphql'), + 'utf8', +); +module.exports.transactionsByOwner = fs.readFileSync( + path.join(__dirname, 'transactionsByOwner.graphql'), + 'utf8', +); +module.exports.transactionsByBlockId = fs.readFileSync( + path.join(__dirname, 'transactionsByBlockId.graphql'), + 'utf8', +); +module.exports.tps = fs.readFileSync( + path.join(__dirname, 'tps.graphql'), + 'utf8', +); +module.exports.getBlocksDashboard = fs.readFileSync( + path.join(__dirname, 'getBlocksDashboard.graphql'), + 'utf8', +); diff --git a/packages/graphql/src/graphql/generated/fuelcore/queries/tps.graphql b/packages/graphql/src/graphql/generated/fuelcore/queries/tps.graphql new file mode 100644 index 000000000..e926f58bb --- /dev/null +++ b/packages/graphql/src/graphql/generated/fuelcore/queries/tps.graphql @@ -0,0 +1,10 @@ +query tps{ + tps{ + nodes{ + start + end + txCount + totalGas + } + } +} \ No newline at end of file diff --git a/packages/graphql/src/graphql/generated/mocks.ts b/packages/graphql/src/graphql/generated/mocks.ts index 364e55959..93b46f3b7 100644 --- a/packages/graphql/src/graphql/generated/mocks.ts +++ b/packages/graphql/src/graphql/generated/mocks.ts @@ -820,7 +820,6 @@ export const aQuery = (overrides?: Partial): { __typename: 'Query' } & search: overrides && overrides.hasOwnProperty('search') ? overrides.search! : aSearchResult(), transaction: overrides && overrides.hasOwnProperty('transaction') ? overrides.transaction! : aTransaction(), transactions: overrides && overrides.hasOwnProperty('transactions') ? overrides.transactions! : aTransactionConnection(), - transactionsByBlockId: overrides && overrides.hasOwnProperty('transactionsByBlockId') ? overrides.transactionsByBlockId! : aTransactionConnection(), transactionsByOwner: overrides && overrides.hasOwnProperty('transactionsByOwner') ? overrides.transactionsByOwner! : aTransactionConnection(), }; }; diff --git a/packages/graphql/src/graphql/generated/sdk-provider.ts b/packages/graphql/src/graphql/generated/sdk-provider.ts index 04526dc4a..da5d18f5e 100644 --- a/packages/graphql/src/graphql/generated/sdk-provider.ts +++ b/packages/graphql/src/graphql/generated/sdk-provider.ts @@ -117,6 +117,19 @@ export enum GQLBlockVersion { V1 = 'V1', } +export type GQLBlocksDashboard = { + __typename: 'BlocksDashboard'; + blockNo: Scalars['U64']['output']; + gasUsed: Scalars['U64']['output']; + producer?: Maybe; + timestamp: Scalars['U64']['output']; +}; + +export type GQLBlocksDashboardConnection = { + __typename: 'BlocksDashboardConnection'; + nodes: Array; +}; + /** Breakpoint, defined as a tuple of contract ID and relative PC offset inside it */ export type GQLBreakpoint = { contract: Scalars['ContractId']['input']; @@ -968,6 +981,7 @@ export type GQLQuery = { estimateGasPrice: GQLEstimateGasPrice; /** Estimate the predicate gas for the provided transaction */ estimatePredicates: GQLTransaction; + getBlocksDashboard: GQLBlocksDashboardConnection; /** Returns true when the GraphQL API is serving requests. */ health: Scalars['Boolean']['output']; latestGasPrice: GQLLatestGasPrice; @@ -983,6 +997,7 @@ export type GQLQuery = { register: Scalars['U64']['output']; relayedTransactionStatus?: Maybe; search?: Maybe; + tps: GQLTpsConnection; transaction?: Maybe; transactions: GQLTransactionConnection; transactionsByBlockId: GQLTransactionConnection; @@ -1319,6 +1334,19 @@ export type GQLSuccessStatus = { transactionId: Scalars['TransactionId']['output']; }; +export type GQLTps = { + __typename: 'TPS'; + end?: Maybe; + start?: Maybe; + totalGas: Scalars['U64']['output']; + txCount: Scalars['U64']['output']; +}; + +export type GQLTpsConnection = { + __typename: 'TPSConnection'; + nodes: Array; +}; + export type GQLTransaction = { __typename: 'Transaction'; _id?: Maybe; @@ -3516,6 +3544,22 @@ export type GQLNodeInfoQuery = { }; }; +export type GQLTpsQueryVariables = Exact<{ [key: string]: never }>; + +export type GQLTpsQuery = { + __typename: 'Query'; + tps: { + __typename: 'TPSConnection'; + nodes: Array<{ + __typename: 'TPS'; + start?: string | null; + end?: string | null; + txCount: string; + totalGas: string; + }>; + }; +}; + export const BalanceItemFragmentDoc = gql` fragment BalanceItem on Balance { amount @@ -5244,6 +5288,18 @@ export const NodeInfoDocument = gql` } } `; +export const TpsDocument = gql` + query tps { + tps { + nodes { + start + end + txCount + totalGas + } + } +} + `; export type SdkFunctionWrapper = ( action: (requestHeaders?: Record) => Promise, @@ -5267,6 +5323,7 @@ const ContractDocumentString = print(ContractDocument); const ContractBalanceDocumentString = print(ContractBalanceDocument); const ContractBalancesDocumentString = print(ContractBalancesDocument); const NodeInfoDocumentString = print(NodeInfoDocument); +const TpsDocumentString = print(TpsDocument); export function getSdk( client: GraphQLClient, withWrapper: SdkFunctionWrapper = defaultWrapper, @@ -5466,6 +5523,27 @@ export function getSdk( variables, ); }, + tps( + variables?: GQLTpsQueryVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise<{ + data: GQLTpsQuery; + errors?: GraphQLError[]; + extensions?: any; + headers: Headers; + status: number; + }> { + return withWrapper( + (wrappedRequestHeaders) => + client.rawRequest(TpsDocumentString, variables, { + ...requestHeaders, + ...wrappedRequestHeaders, + }), + 'tps', + 'query', + variables, + ); + }, }; } export type Sdk = ReturnType; diff --git a/packages/graphql/src/graphql/generated/sdk.ts b/packages/graphql/src/graphql/generated/sdk.ts index 7d00e6408..ffad30630 100644 --- a/packages/graphql/src/graphql/generated/sdk.ts +++ b/packages/graphql/src/graphql/generated/sdk.ts @@ -117,6 +117,19 @@ export enum GQLBlockVersion { V1 = 'V1', } +export type GQLBlocksDashboard = { + __typename: 'BlocksDashboard'; + blockNo: Scalars['U64']['output']; + gasUsed: Scalars['U64']['output']; + producer?: Maybe; + timestamp: Scalars['U64']['output']; +}; + +export type GQLBlocksDashboardConnection = { + __typename: 'BlocksDashboardConnection'; + nodes: Array; +}; + /** Breakpoint, defined as a tuple of contract ID and relative PC offset inside it */ export type GQLBreakpoint = { contract: Scalars['ContractId']['input']; @@ -968,6 +981,7 @@ export type GQLQuery = { estimateGasPrice: GQLEstimateGasPrice; /** Estimate the predicate gas for the provided transaction */ estimatePredicates: GQLTransaction; + getBlocksDashboard: GQLBlocksDashboardConnection; /** Returns true when the GraphQL API is serving requests. */ health: Scalars['Boolean']['output']; latestGasPrice: GQLLatestGasPrice; @@ -983,6 +997,7 @@ export type GQLQuery = { register: Scalars['U64']['output']; relayedTransactionStatus?: Maybe; search?: Maybe; + tps: GQLTpsConnection; transaction?: Maybe; transactions: GQLTransactionConnection; transactionsByBlockId: GQLTransactionConnection; @@ -1319,6 +1334,19 @@ export type GQLSuccessStatus = { transactionId: Scalars['TransactionId']['output']; }; +export type GQLTps = { + __typename: 'TPS'; + end?: Maybe; + start?: Maybe; + totalGas: Scalars['U64']['output']; + txCount: Scalars['U64']['output']; +}; + +export type GQLTpsConnection = { + __typename: 'TPSConnection'; + nodes: Array; +}; + export type GQLTransaction = { __typename: 'Transaction'; _id?: Maybe; @@ -2689,6 +2717,24 @@ export type GQLContractBalancesQuery = { }; }; +export type GQLGetBlocksDashboardQueryVariables = Exact<{ + [key: string]: never; +}>; + +export type GQLGetBlocksDashboardQuery = { + __typename: 'Query'; + getBlocksDashboard: { + __typename: 'BlocksDashboardConnection'; + nodes: Array<{ + __typename: 'BlocksDashboard'; + timestamp: string; + gasUsed: string; + blockNo: string; + producer?: string | null; + }>; + }; +}; + export type GQLPredicateQueryVariables = Exact<{ address: Scalars['String']['input']; }>; @@ -2782,6 +2828,22 @@ export type GQLSearchQuery = { } | null; }; +export type GQLTpsQueryVariables = Exact<{ [key: string]: never }>; + +export type GQLTpsQuery = { + __typename: 'Query'; + tps: { + __typename: 'TPSConnection'; + nodes: Array<{ + __typename: 'TPS'; + start?: string | null; + end?: string | null; + txCount: string; + totalGas: string; + }>; + }; +}; + export type GQLTransactionDetailsQueryVariables = Exact<{ id: Scalars['TransactionId']['input']; }>; @@ -6270,6 +6332,18 @@ export const ContractBalancesDocument = gql` } } ${ContractBalanceConnectionNodeFragmentDoc}`; +export const GetBlocksDashboardDocument = gql` + query getBlocksDashboard { + getBlocksDashboard { + nodes { + timestamp + gasUsed + blockNo + producer + } + } +} + `; export const PredicateDocument = gql` query predicate($address: String!) { predicate(address: $address) { @@ -6315,6 +6389,18 @@ export const SearchDocument = gql` } } `; +export const TpsDocument = gql` + query tps { + tps { + nodes { + start + end + txCount + totalGas + } + } +} + `; export const TransactionDetailsDocument = gql` query transactionDetails($id: TransactionId!) { transaction(id: $id) { @@ -6385,9 +6471,11 @@ const ChainDocumentString = print(ChainDocument); const CoinsDocumentString = print(CoinsDocument); const ContractDocumentString = print(ContractDocument); const ContractBalancesDocumentString = print(ContractBalancesDocument); +const GetBlocksDashboardDocumentString = print(GetBlocksDashboardDocument); const PredicateDocumentString = print(PredicateDocument); const RecentTransactionsDocumentString = print(RecentTransactionsDocument); const SearchDocumentString = print(SearchDocument); +const TpsDocumentString = print(TpsDocument); const TransactionDetailsDocumentString = print(TransactionDetailsDocument); const TransactionsByBlockIdDocumentString = print( TransactionsByBlockIdDocument, @@ -6548,6 +6636,28 @@ export function getSdk( variables, ); }, + getBlocksDashboard( + variables?: GQLGetBlocksDashboardQueryVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise<{ + data: GQLGetBlocksDashboardQuery; + errors?: GraphQLError[]; + extensions?: any; + headers: Headers; + status: number; + }> { + return withWrapper( + (wrappedRequestHeaders) => + client.rawRequest( + GetBlocksDashboardDocumentString, + variables, + { ...requestHeaders, ...wrappedRequestHeaders }, + ), + 'getBlocksDashboard', + 'query', + variables, + ); + }, predicate( variables: GQLPredicateQueryVariables, requestHeaders?: GraphQLClientRequestHeaders, @@ -6613,6 +6723,27 @@ export function getSdk( variables, ); }, + tps( + variables?: GQLTpsQueryVariables, + requestHeaders?: GraphQLClientRequestHeaders, + ): Promise<{ + data: GQLTpsQuery; + errors?: GraphQLError[]; + extensions?: any; + headers: Headers; + status: number; + }> { + return withWrapper( + (wrappedRequestHeaders) => + client.rawRequest(TpsDocumentString, variables, { + ...requestHeaders, + ...wrappedRequestHeaders, + }), + 'tps', + 'query', + variables, + ); + }, transactionDetails( variables: GQLTransactionDetailsQueryVariables, requestHeaders?: GraphQLClientRequestHeaders, diff --git a/packages/graphql/src/graphql/queries/provider/tps.graphql b/packages/graphql/src/graphql/queries/provider/tps.graphql new file mode 100644 index 000000000..3599163b8 --- /dev/null +++ b/packages/graphql/src/graphql/queries/provider/tps.graphql @@ -0,0 +1,10 @@ +query tps{ + tps{ + nodes { + start + end + txCount + totalGas + } + } +} diff --git a/packages/graphql/src/graphql/queries/sdk/getBlocksDashboard.graphql b/packages/graphql/src/graphql/queries/sdk/getBlocksDashboard.graphql new file mode 100644 index 000000000..e1ad2f02e --- /dev/null +++ b/packages/graphql/src/graphql/queries/sdk/getBlocksDashboard.graphql @@ -0,0 +1,10 @@ +query getBlocksDashboard{ + getBlocksDashboard{ + nodes { + timestamp + gasUsed + blockNo + producer + } + } +} \ No newline at end of file diff --git a/packages/graphql/src/graphql/queries/sdk/tps.graphql b/packages/graphql/src/graphql/queries/sdk/tps.graphql new file mode 100644 index 000000000..3599163b8 --- /dev/null +++ b/packages/graphql/src/graphql/queries/sdk/tps.graphql @@ -0,0 +1,10 @@ +query tps{ + tps{ + nodes { + start + end + txCount + totalGas + } + } +} diff --git a/packages/graphql/src/graphql/resolvers/BlockResolver.ts b/packages/graphql/src/graphql/resolvers/BlockResolver.ts index 0ca336c7e..e309db74b 100644 --- a/packages/graphql/src/graphql/resolvers/BlockResolver.ts +++ b/packages/graphql/src/graphql/resolvers/BlockResolver.ts @@ -12,6 +12,8 @@ type Source = GQLBlock; type Params = { blocks: GQLQueryBlocksArgs; block: GQLQueryBlockArgs; + tps: null; + getBlocksDashboard: null; }; export class BlockResolver { @@ -21,6 +23,8 @@ export class BlockResolver { Query: { block: resolvers.block, blocks: resolvers.blocks, + tps: resolvers.tps, + getBlocksDashboard: resolvers.getBlocksDashboard, }, }; } @@ -56,4 +60,18 @@ export class BlockResolver { ); return blocks; } + + async getBlocksDashboard(_: Source, _params: Params['getBlocksDashboard']) { + const blockDAO = new BlockDAO(); + const blocks = await blockDAO.getBlocksDashboard(); + + return blocks; + } + + async tps(_: Source, _params: Params['tps']) { + const blockDAO = new BlockDAO(); + const tps = await blockDAO.tps(); + + return tps; + } } diff --git a/packages/graphql/src/graphql/resolvers/getBlocksDashboard.graphql b/packages/graphql/src/graphql/resolvers/getBlocksDashboard.graphql new file mode 100644 index 000000000..e1ad2f02e --- /dev/null +++ b/packages/graphql/src/graphql/resolvers/getBlocksDashboard.graphql @@ -0,0 +1,10 @@ +query getBlocksDashboard{ + getBlocksDashboard{ + nodes { + timestamp + gasUsed + blockNo + producer + } + } +} \ No newline at end of file diff --git a/packages/graphql/src/graphql/schemas/explorer.graphql b/packages/graphql/src/graphql/schemas/explorer.graphql index 9f1d4bc44..efe1b21b4 100644 --- a/packages/graphql/src/graphql/schemas/explorer.graphql +++ b/packages/graphql/src/graphql/schemas/explorer.graphql @@ -223,4 +223,28 @@ type Query { search(query: String!): SearchResult predicate(address: String!): PredicateItem + tps: TPSConnection! + getBlocksDashboard: BlocksDashboardConnection! } + +type TPS { + start: String + end: String + txCount: U64! + totalGas: U64! +} + +type TPSConnection { + nodes: [TPS!]! +} + +type BlocksDashboard { + timestamp: U64! + gasUsed: U64! + blockNo: U64! + producer: String +} + +type BlocksDashboardConnection { + nodes: [BlocksDashboard!]! +} \ No newline at end of file diff --git a/packages/graphql/src/graphql/schemas/fuelcore.graphql b/packages/graphql/src/graphql/schemas/fuelcore.graphql index ab84ad760..5e880dd0d 100644 --- a/packages/graphql/src/graphql/schemas/fuelcore.graphql +++ b/packages/graphql/src/graphql/schemas/fuelcore.graphql @@ -781,6 +781,8 @@ type Query { transactions(after: String, before: String, first: Int, last: Int): TransactionConnection! transactionsByOwner(after: String, before: String, first: Int, last: Int, owner: Address!): TransactionConnection! transactionsByBlockId(after: String, before: String, first: Int, last: Int, blockId: String!): TransactionConnection! + tps: TPSConnection! + getBlocksDashboard : BlocksDashboardConnection! } type Receipt { @@ -1021,4 +1023,26 @@ type VariableOutput { amount: U64! assetId: AssetId! to: Address! +} + +type TPS { + start: String + end: String + txCount: U64! + totalGas: U64! +} + +type TPSConnection { + nodes: [TPS!]! +} + +type BlocksDashboard { + timestamp: U64! + gasUsed: U64! + blockNo: U64! + producer: String +} + +type BlocksDashboardConnection { + nodes: [BlocksDashboard!]! } \ No newline at end of file diff --git a/packages/graphql/src/infra/dao/Block.ts b/packages/graphql/src/infra/dao/Block.ts index e2849909e..90c653c10 100644 --- a/packages/graphql/src/infra/dao/Block.ts +++ b/packages/graphql/src/infra/dao/Block.ts @@ -62,4 +62,15 @@ export default class Block { transactions: this.transactions.map((t) => t.toGQLNode()), }; } + + toTPSNode() { + const _data = this.data; + return { + blockNo: String(this.id), + producer: this.producer, + timestamp: this.time.rawUnix, + gasUsed: `${this.totalGasUsed}`, + tps: String(this.transactions.length), + }; + } } diff --git a/packages/graphql/src/infra/dao/BlockDAO.ts b/packages/graphql/src/infra/dao/BlockDAO.ts index 33e1c517f..cf9436204 100644 --- a/packages/graphql/src/infra/dao/BlockDAO.ts +++ b/packages/graphql/src/infra/dao/BlockDAO.ts @@ -1,6 +1,7 @@ import { DatabaseConnection } from '../database/DatabaseConnection'; import PaginatedParams from '../paginator/PaginatedParams'; import Block from './Block'; +import { createIntervals, roundToNearest } from './utils'; export default class BlockDAO { databaseConnection: DatabaseConnection; @@ -121,4 +122,93 @@ export default class BlockDAO { if (!blockData) return; return new Block(blockData); } + + async getBlocksDashboard() { + const blocksData = await this.databaseConnection.query( + ` + select + b._id AS blockno, + b.gas_used AS gasused, + b.producer, + b.timestamp AS timestamp + from + indexer.blocks b + order by + b._id desc + limit 6 + `, + [], + ); + + const formattedBlocksData = blocksData.map((block) => ({ + timestamp: new Date(Number(block.timestamp)).getTime(), + gasUsed: Number(block.gasused), + blockNo: block.blockno, + producer: block.producer, + })); + + return { + nodes: formattedBlocksData, + }; + } + + async tps() { + const currentTime = new Date(); + const timeMinusOneDay = new Date( + currentTime.getTime() - 24 * 60 * 60 * 1000, + ); + const timeMinusOneDayRoundDown = new Date( + roundToNearest(timeMinusOneDay.getTime()), + ); + const timeMinusOneDayRoundDownISO = timeMinusOneDayRoundDown.toISOString(); + + const blocksData = await this.databaseConnection.query( + ` + SELECT + b.timestamp AS timestamp, + b.data->'header'->>'transactionsCount' AS tps, + b.gas_used AS gasused + FROM + indexer.blocks b + WHERE + b.timestamp >= $1 + ORDER BY _id asc; + `, + [timeMinusOneDayRoundDownISO], + ); + + if (blocksData.length === 0) { + return { nodes: [] }; + } + + const lastTimestamp = new Date( + Number(blocksData[blocksData.length - 1].timestamp), + ).getTime(); + const firstTimestamp = new Date(Number(blocksData[0].timestamp)).getTime(); + + const intervals = createIntervals(firstTimestamp, lastTimestamp, 'hour', 1); + + // Process blocks and put them into the correct interval + blocksData.forEach((block) => { + const blockTimestamp = new Date(Number(block.timestamp)).getTime(); + const txCount = Number(block.tps); + const gasUsed = Number(block.gasused); + + // Find the correct interval for the current block + for (const interval of intervals) { + const intervalStart = new Date(interval.start).getTime(); + const intervalEnd = new Date(interval.end).getTime(); + + if (blockTimestamp >= intervalStart && blockTimestamp < intervalEnd) { + interval.txCount += txCount; + interval.totalGas += gasUsed; + break; + } + } + }); + + return { + nodes: intervals, + }; + } } diff --git a/packages/graphql/src/infra/dao/utils.ts b/packages/graphql/src/infra/dao/utils.ts new file mode 100644 index 000000000..fef2cb26a --- /dev/null +++ b/packages/graphql/src/infra/dao/utils.ts @@ -0,0 +1,49 @@ +export function roundToNearest(time: number, roundUp = false): number { + const msInHour = 60 * 60 * 1000; + return roundUp + ? Math.ceil(time / msInHour) * msInHour + : Math.floor(time / msInHour) * msInHour; +} + +// General interval creation function +export function createIntervals( + startTime: number, + endTime: number, + unit: 'minute' | 'hour' | 'day', + intervalSize: number, +): Array<{ start: Date; end: Date; txCount: number; totalGas: number }> { + const roundedStartTime = roundToNearest(startTime); + const roundedEndTime = roundToNearest(endTime, true); + + const intervals: Array<{ + start: Date; + end: Date; + txCount: number; + totalGas: number; + }> = []; + + let currentTime = roundedStartTime; + + // Handle minute, hour, and day intervals + const msInUnit = { + minute: 60 * 1000, + hour: 60 * 60 * 1000, + day: 24 * 60 * 60 * 1000, + }; + + const intervalDuration = intervalSize * msInUnit[unit]; + + while (currentTime < roundedEndTime) { + const startInterval = new Date(currentTime); + const endInterval = new Date(currentTime + intervalDuration); + intervals.push({ + start: startInterval, + end: endInterval, + txCount: 0, + totalGas: 0, + }); + currentTime += intervalDuration; + } + + return intervals; +} diff --git a/packages/ui/package.json b/packages/ui/package.json index f1419af1c..386b6740b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -59,8 +59,8 @@ "@radix-ui/colors": "3.0.0", "@radix-ui/react-accordion": "1.1.2", "@radix-ui/react-aspect-ratio": "1.1.0", - "@radix-ui/react-portal": "1.1.1", "@radix-ui/react-dialog": "1.1.1", + "@radix-ui/react-portal": "1.1.1", "@radix-ui/react-slot": "1.0.2", "@radix-ui/react-toast": "1.2.1", "@radix-ui/themes": "3.1.1", @@ -69,13 +69,16 @@ "@tailwindcss/typography": "0.5.10", "clsx": "2.1.0", "csstype": "3.1.3", + "embla-carousel-react": "8.2.1", "framer-motion": "11.0.5", "geist": "1.2.2", "modern-normalize": "2.0.0", "next": "14.1.0", "react": "18.2.0", "react-aria": "3.32.1", + "react-data-table-component": "7.6.2", "react-dom": "18.2.0", + "react-paginate": "8.2.0", "react-stately": "3.29.1", "react-use": "17.5.0", "tailwind-variants": "0.1.20", diff --git a/packages/ui/src/components/Box/RoundedContainer.tsx b/packages/ui/src/components/Box/RoundedContainer.tsx new file mode 100644 index 000000000..c652a60d2 --- /dev/null +++ b/packages/ui/src/components/Box/RoundedContainer.tsx @@ -0,0 +1,25 @@ +import { Box as RadixBox } from '@radix-ui/themes'; + +import { tv } from 'tailwind-variants'; +import { createComponent } from '../../utils/component'; +import type { PropsOf, WithAsProps } from '../../utils/types'; + +export type RoundedContainerProps = WithAsProps & PropsOf; + +const styles = tv({ + slots: { + root: 'rounded-[13px] p-4 bg-white dark:bg-card-bg', + }, +}); + +export const RoundedContainer = createComponent< + RoundedContainerProps, + typeof RadixBox +>({ + id: 'RoundedContainer', + baseElement: RadixBox, + className: ({ className }) => styles().root({ className }), + render: (Comp, { children, ...props }) => { + return {children}; + }, +}); diff --git a/packages/ui/src/components/Box/index.tsx b/packages/ui/src/components/Box/index.tsx index d0dc5bdda..392656237 100644 --- a/packages/ui/src/components/Box/index.tsx +++ b/packages/ui/src/components/Box/index.tsx @@ -15,3 +15,6 @@ export type { VStackProps } from './VStack'; export { HStack } from './HStack'; export type { HStackProps } from './HStack'; + +export { RoundedContainer } from './RoundedContainer'; +export type { RoundedContainerProps } from './RoundedContainer'; diff --git a/packages/ui/src/components/Carousel/Carousel.tsx b/packages/ui/src/components/Carousel/Carousel.tsx new file mode 100644 index 000000000..f6d71d73f --- /dev/null +++ b/packages/ui/src/components/Carousel/Carousel.tsx @@ -0,0 +1,195 @@ +'use client'; + +import useEmblaCarousel, { + type UseEmblaCarouselType, +} from 'embla-carousel-react'; +import * as React from 'react'; + +import { cx } from '../../utils/css'; + +type CarouselApi = UseEmblaCarouselType[1]; +export type UseCarouselParameters = Parameters; +export type CarouselOptions = UseCarouselParameters[0]; +export type CarouselPlugin = UseCarouselParameters[1]; + +export type CarouselProps = { + opts?: CarouselOptions; + plugins?: CarouselPlugin; + orientation?: 'horizontal' | 'vertical'; + setApi?: (api: CarouselApi) => void; +}; + +export type CarouselContextProps = { + carouselRef: ReturnType[0]; + api: ReturnType[1]; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; +} & CarouselProps; + +const CarouselContext = React.createContext(null); + +function useCarousel() { + const context = React.useContext(CarouselContext); + + if (!context) { + throw new Error('useCarousel must be used within a '); + } + + return context; +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & CarouselProps +>( + ( + { + orientation = 'horizontal', + opts, + setApi, + plugins, + className, + children, + ...props + }, + ref, + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === 'horizontal' ? 'x' : 'y', + }, + plugins, + ); + const [canScrollPrev, setCanScrollPrev] = React.useState(false); + const [canScrollNext, setCanScrollNext] = React.useState(false); + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return; + } + + setCanScrollPrev(api.canScrollPrev()); + setCanScrollNext(api.canScrollNext()); + }, []); + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev(); + }, [api]); + + const scrollNext = React.useCallback(() => { + api?.scrollNext(); + }, [api]); + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'ArrowLeft') { + event.preventDefault(); + scrollPrev(); + } else if (event.key === 'ArrowRight') { + event.preventDefault(); + scrollNext(); + } + }, + [scrollPrev, scrollNext], + ); + + React.useEffect(() => { + if (!api || !setApi) { + return; + } + + setApi(api); + }, [api, setApi]); + + React.useEffect(() => { + if (!api) { + return; + } + + onSelect(api); + api.on('reInit', onSelect); + api.on('select', onSelect); + + return () => { + api?.off('select', onSelect); + }; + }, [api, onSelect]); + + return ( + +
+ {children} +
+
+ ); + }, +); +Carousel.displayName = 'Carousel'; + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel(); + + return ( +
+
+
+ ); +}); +CarouselContent.displayName = 'CarouselContent'; + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel(); + + return ( +
+ ); +}); +CarouselItem.displayName = 'CarouselItem'; + +export { type CarouselApi, Carousel, CarouselContent, CarouselItem }; diff --git a/packages/ui/src/components/Carousel/index.tsx b/packages/ui/src/components/Carousel/index.tsx new file mode 100644 index 000000000..7cbb397d3 --- /dev/null +++ b/packages/ui/src/components/Carousel/index.tsx @@ -0,0 +1,11 @@ +'use client'; + +export { Carousel, CarouselContent, CarouselItem } from './Carousel'; + +export type { + CarouselContextProps, + CarouselOptions, + CarouselPlugin, + CarouselProps, + UseCarouselParameters, +} from './Carousel'; diff --git a/packages/ui/src/components/Charts/Charts.tsx b/packages/ui/src/components/Charts/Charts.tsx new file mode 100644 index 000000000..53431a871 --- /dev/null +++ b/packages/ui/src/components/Charts/Charts.tsx @@ -0,0 +1,366 @@ +'use client'; + +import * as React from 'react'; +import * as RechartsPrimitive from 'recharts'; + +import { cx } from '../../utils/css'; + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: '', dark: '.dark' } as const; + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode; + icon?: React.ComponentType; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ); +}; + +type ChartContextProps = { + config: ChartConfig; +}; + +const ChartContext = React.createContext(null); + +function useChart() { + const context = React.useContext(ChartContext); + + if (!context) { + throw new Error('useChart must be used within a '); + } + + return context; +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> & { + config: ChartConfig; + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >['children']; + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId(); + const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`; + + return ( + +
+ + + {children} + +
+
+ ); +}); +ChartContainer.displayName = 'Chart'; + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([_, config]) => config.theme || config.color, + ); + + if (!colorConfig.length) { + return null; + } + + return ( +