diff --git a/app/NFT/collection/[collectionId]/page.tsx b/app/NFT/collection/[collectionId]/page.tsx index 059d7d0..54b22af 100644 --- a/app/NFT/collection/[collectionId]/page.tsx +++ b/app/NFT/collection/[collectionId]/page.tsx @@ -225,7 +225,6 @@ export default function CollectionDetailsPage() { // Add network as a filter attribute attributeMap['Network'] = [ networkId === '0x1' ? 'Ethereum' : - networkId === '0xaa36a7' ? 'Sepolia' : networkId === '0x38' ? 'BNB Chain' : 'BNB Testnet' ]; @@ -339,7 +338,6 @@ export default function CollectionDetailsPage() { const getExplorerLink = (address: string) => { const chainConfig = { '0x1': 'https://etherscan.io', - '0xaa36a7': 'https://sepolia.etherscan.io', '0x38': 'https://bscscan.com', '0x61': 'https://testnet.bscscan.com', }; @@ -352,7 +350,6 @@ export default function CollectionDetailsPage() { const getNetworkName = () => { const networks = { '0x1': 'Ethereum', - '0xaa36a7': 'Sepolia', '0x38': 'BNB Chain', '0x61': 'BNB Testnet', }; diff --git a/app/NFT/collection/page.tsx b/app/NFT/collection/page.tsx index 45e1427..11b9bae 100644 --- a/app/NFT/collection/page.tsx +++ b/app/NFT/collection/page.tsx @@ -202,17 +202,24 @@ export default function NFTCollectionPage() { method: 'eth_chainId', }); setChainId(chainId); + // Load collections immediately after setting chainId + loadCollections(chainId); + // Initial trending data - load for default period + loadTrendingCollections('24h'); } catch (error) { console.error('Error checking network:', error); + // If there's an error, still load with default chainId + loadCollections(chainId); + loadTrendingCollections('24h'); } + } else { + // If no window.ethereum, load with default chainId + loadCollections(chainId); + loadTrendingCollections('24h'); } }; checkNetwork(); - loadCollections(chainId); - - // Initial trending data - loadTrendingCollections('24h'); }, []); // Load user NFTs when account or chain changes @@ -304,6 +311,29 @@ export default function NFTCollectionPage() { // Load trending collections const loadTrendingCollections = useCallback(async (period: string) => { try { + // Create placeholder data with empty array if collections not loaded yet + if (collections.length === 0) { + // Set mock empty data with period to display the section even when data is loading + setTrendingData({ + period, + data: [ + { + id: 'loading-1', + name: 'Loading...', + imageUrl: '/images/placeholder-nft.png', + chain: chainId, + floorPrice: '0.00', + priceChange: 0, + volume: '0.00', + volumeChange: 0, + }, + // Add more placeholder items as needed + ] + }); + // Return early - real data will be loaded once collections are available + return; + } + // In real app this would fetch real data const mockTrendingData = { period, @@ -323,7 +353,14 @@ export default function NFTCollectionPage() { } catch (error) { console.error('Error loading trending data:', error); } - }, [collections]); + }, [collections, chainId]); + + useEffect(() => { + // When collections are loaded, update the trending data + if (collections.length > 0) { + loadTrendingCollections(trendingPeriod); + } + }, [collections, loadTrendingCollections, trendingPeriod]); const handleNetworkChange = (networkId: string) => { setChainId(networkId); @@ -516,52 +553,63 @@ export default function NFTCollectionPage() { - {trendingData.data.map((item, i) => ( - handleCardClick(item.id)} - initial={{ opacity: 0, y: 10 }} - animate={{ opacity: 1, y: 0 }} - transition={{ delay: 0.3 + i * 0.05 }} - > - {i + 1} - -
-
- {item.name} -
-
-
- {item.name} - {(i === 0 || i === 2) && } + {trendingData.data.length > 0 ? ( + trendingData.data.map((item, i) => ( + handleCardClick(item.id)} + initial={{ opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.3 + i * 0.05 }} + > + {i + 1} + +
+
+ {item.name}
-
- {getNetworkName(item.chain)} +
+
+ {item.name} + {(i === 0 || i === 2) && } +
+
+ {getNetworkName(item.chain)} +
+ + + {item.floorPrice} {item.chain === '0x1' || item.chain === '0xaa36a7' ? 'ETH' : 'BNB'} + + = 0 ? 'text-green-500' : 'text-red-500'}`}> + {item.priceChange >= 0 ? '+' : ''}{item.priceChange.toFixed(2)}% + + + {item.volume} {item.chain === '0x1' || item.chain === '0xaa36a7' ? 'ETH' : 'BNB'} + + = 0 ? 'text-green-500' : 'text-red-500'}`}> + {item.volumeChange >= 0 ? '+' : ''}{item.volumeChange.toFixed(2)}% + + + )) + ) : ( + + +
+
+ Loading trending collections...
- - {item.floorPrice} {item.chain === '0x1' || item.chain === '0xaa36a7' ? 'ETH' : 'BNB'} - - = 0 ? 'text-green-500' : 'text-red-500'}`}> - {item.priceChange >= 0 ? '+' : ''}{item.priceChange.toFixed(2)}% - - - {item.volume} {item.chain === '0x1' || item.chain === '0xaa36a7' ? 'ETH' : 'BNB'} - - = 0 ? 'text-green-500' : 'text-red-500'}`}> - {item.volumeChange >= 0 ? '+' : ''}{item.volumeChange.toFixed(2)}% - - - ))} + + )}
diff --git a/app/api/alchemy-block-txns/route.ts b/app/api/alchemy-block-txns/route.ts new file mode 100644 index 0000000..6d64c40 --- /dev/null +++ b/app/api/alchemy-block-txns/route.ts @@ -0,0 +1,114 @@ +import { NextResponse } from "next/server"; + +const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY; +const ALCHEMY_API_URL = `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`; + +interface Transaction { + blockHash: string; + blockNumber: string; + from: string; + gas: string; + gasPrice: string; + hash: string; + input: string; + nonce: string; + to: string; + transactionIndex: string; + value: string; + type: string; + timestamp: number; +} + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const blockNumber = searchParams.get("blockNumber"); + + if (!blockNumber) { + return NextResponse.json( + { error: "Block number is required" }, + { status: 400 } + ); + } + + try { + // First, get block data to get timestamp + const blockResponse = await fetch(ALCHEMY_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'eth_getBlockByNumber', + params: [ + `0x${Number(blockNumber).toString(16)}`, + false + ] + }) + }); + + const blockData = await blockResponse.json(); + + if (!blockData.result) { + return NextResponse.json( + { error: "Block not found" }, + { status: 404 } + ); + } + + const timestamp = parseInt(blockData.result.timestamp, 16); + + // Get all transactions from the block + const txnsResponse = await fetch(ALCHEMY_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'eth_getBlockByNumber', + params: [ + `0x${Number(blockNumber).toString(16)}`, + true + ] + }) + }); + + const txnsData = await txnsResponse.json(); + + if (!txnsData.result || !txnsData.result.transactions) { + return NextResponse.json( + { error: "Failed to fetch transactions" }, + { status: 500 } + ); + } + + const transactions: Transaction[] = txnsData.result.transactions.map((tx: any) => ({ + blockHash: tx.blockHash, + blockNumber: parseInt(tx.blockNumber, 16).toString(), + from: tx.from, + gas: parseInt(tx.gas, 16).toString(), + gasPrice: parseInt(tx.gasPrice, 16).toString(), + hash: tx.hash, + input: tx.input, + nonce: parseInt(tx.nonce, 16).toString(), + to: tx.to || '0x0', // Contract creation if no 'to' address + transactionIndex: parseInt(tx.transactionIndex, 16).toString(), + value: (parseInt(tx.value, 16) / 1e18).toString(), // Convert from Wei to ETH + type: parseInt(tx.type, 16).toString(), + timestamp: timestamp + })); + + return NextResponse.json({ + blockNumber, + timestamp, + transactions, + total: transactions.length + }); + + } catch (error) { + console.error("Error fetching block transactions:", error); + return NextResponse.json( + { error: "Failed to fetch block transactions" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/alchemy-block/route.ts b/app/api/alchemy-block/route.ts new file mode 100644 index 0000000..58e89c1 --- /dev/null +++ b/app/api/alchemy-block/route.ts @@ -0,0 +1,84 @@ +import { NextResponse } from "next/server"; + +const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY; +const ALCHEMY_API_URL = `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`; + +interface BlockDetails { + number: string; + hash: string; + parentHash: string; + timestamp: string; + nonce: string; + difficulty: string; + gasLimit: string; + gasUsed: string; + miner: string; + baseFeePerGas: string; + extraData: string; + transactions: string[]; + size: string; +} + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const blockNumber = searchParams.get("blockNumber"); + const blockHash = searchParams.get("blockHash"); + + if (!blockNumber && !blockHash) { + return NextResponse.json( + { error: "Block number or hash is required" }, + { status: 400 } + ); + } + + try { + const blockResponse = await fetch(ALCHEMY_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'eth_getBlockByNumber', + params: [ + blockNumber ? `0x${Number(blockNumber).toString(16)}` : blockHash, + true + ] + }) + }); + + const blockData = await blockResponse.json(); + + if (!blockData.result) { + return NextResponse.json( + { error: "Block not found" }, + { status: 404 } + ); + } + + const block: BlockDetails = { + number: parseInt(blockData.result.number, 16).toString(), + hash: blockData.result.hash, + parentHash: blockData.result.parentHash, + timestamp: new Date(parseInt(blockData.result.timestamp, 16) * 1000).toISOString(), + nonce: blockData.result.nonce, + difficulty: parseInt(blockData.result.difficulty, 16).toString(), + gasLimit: parseInt(blockData.result.gasLimit, 16).toString(), + gasUsed: parseInt(blockData.result.gasUsed, 16).toString(), + miner: blockData.result.miner, + baseFeePerGas: blockData.result.baseFeePerGas + ? parseInt(blockData.result.baseFeePerGas, 16).toString() + : "0", + extraData: blockData.result.extraData, + transactions: blockData.result.transactions.map((tx: any) => tx.hash), + size: parseInt(blockData.result.size, 16).toString() + }; + + return NextResponse.json(block); + } catch (error) { + console.error("Error fetching block details:", error); + return NextResponse.json( + { error: "Failed to fetch block details" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/alchemy-token/route.ts b/app/api/alchemy-token/route.ts new file mode 100644 index 0000000..042dd8e --- /dev/null +++ b/app/api/alchemy-token/route.ts @@ -0,0 +1,170 @@ +import { NextResponse } from "next/server"; + +const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY; +const ALCHEMY_API_URL = `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`; + +interface TokenMetadata { + name: string; + symbol: string; + decimals: number; + logo: string | null; + totalSupply?: string; +} + +interface TokenTransfer { + hash: string; + from: string; + to: string; + value: string; + blockNumber: string; + timestamp: string; +} + +interface TokenDetails { + name: string; + symbol: string; + decimals: number; + address: string; + totalSupply: string; + logo: string; + holders: number; + transfers: number; + lastUpdated: string; + contractDeployed: string; + implementation?: string; + isProxy: boolean; + recentTransfers: TokenTransfer[]; + priceUSD?: string; + volume24h?: string; + marketCap?: string; +} + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const address = searchParams.get("address"); + + if (!address) { + return NextResponse.json({ error: "Token address is required" }, { status: 400 }); + } + + try { + // Get token metadata + const metadataResponse = await fetch(ALCHEMY_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'alchemy_getTokenMetadata', + params: [address] + }) + }); + const metadataData = await metadataResponse.json(); + + // Get token total supply + const supplyResponse = await fetch(ALCHEMY_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 2, + method: 'eth_call', + params: [{ + to: address, + data: '0x18160ddd' // totalSupply() + }, 'latest'] + }) + }); + const supplyData = await supplyResponse.json(); + + // Get recent token transfers + const transfersResponse = await fetch(ALCHEMY_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 3, + method: 'alchemy_getAssetTransfers', + params: [{ + fromBlock: "0x0", + toBlock: "latest", + contractAddresses: [address], + category: ["erc20"], + withMetadata: true, + maxCount: "0x14", // Fetch last 20 transfers + order: "desc" + }] + }) + }); + const transfersData = await transfersResponse.json(); + + // Format recent transfers + const recentTransfers = transfersData.result?.transfers?.map((transfer: any) => ({ + hash: transfer.hash, + from: transfer.from, + to: transfer.to, + value: transfer.value, + blockNumber: transfer.blockNum, + timestamp: transfer.metadata.blockTimestamp + })) || []; + + // Get price data from CoinGecko + let priceData = null; + try { + const priceResponse = await fetch( + `https://api.coingecko.com/api/v3/simple/token_price/ethereum?contract_addresses=${address}&vs_currencies=usd&include_24hr_vol=true&include_market_cap=true` + ); + priceData = await priceResponse.json(); + } catch (e) { + console.error("Error fetching price data:", e); + } + + // Get contract creation info + const deploymentResponse = await fetch(ALCHEMY_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 4, + method: 'eth_getCode', + params: [address, 'latest'] + }) + }); + const deploymentData = await deploymentResponse.json(); + const isProxy = deploymentData.result?.length > 2; // Simple check for proxy contract + + if (!metadataData.result || !supplyData.result) { + return NextResponse.json({ error: "Token not found" }, { status: 404 }); + } + + const metadata: TokenMetadata = metadataData.result; + const totalSupply = parseInt(supplyData.result, 16).toString(); + const transfers = recentTransfers.length; + + const tokenDetails: TokenDetails = { + name: metadata.name || 'Unknown Token', + symbol: metadata.symbol || 'UNKNOWN', + decimals: metadata.decimals || 18, + address: address, + totalSupply, + logo: metadata.logo || '/placeholder-token.png', + holders: 0, // Would need separate API call + transfers, + lastUpdated: new Date().toISOString(), + contractDeployed: new Date().toISOString(), + isProxy, + recentTransfers, + priceUSD: priceData?.[address.toLowerCase()]?.usd?.toString(), + volume24h: priceData?.[address.toLowerCase()]?.usd_24h_vol?.toString(), + marketCap: priceData?.[address.toLowerCase()]?.usd_market_cap?.toString() + }; + + return NextResponse.json(tokenDetails); + } catch (error) { + console.error("Error fetching token details:", error); + return NextResponse.json( + { error: "Failed to fetch token details" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/alchemy-txnhash/route.ts b/app/api/alchemy-txnhash/route.ts new file mode 100644 index 0000000..9b93fc6 --- /dev/null +++ b/app/api/alchemy-txnhash/route.ts @@ -0,0 +1,95 @@ +import { NextResponse } from "next/server"; + +const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY; +const ALCHEMY_API_URL = `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const hash = searchParams.get("hash"); + + if (!hash) { + return NextResponse.json({ error: "Transaction hash is required" }, { status: 400 }); + } + + try { + // Get transaction details + const txResponse = await fetch(ALCHEMY_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'eth_getTransactionByHash', + params: [hash] + }) + }); + const txData = await txResponse.json(); + + // Get transaction receipt + const receiptResponse = await fetch(ALCHEMY_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 2, + method: 'eth_getTransactionReceipt', + params: [hash] + }) + }); + const receiptData = await receiptResponse.json(); + + if (!txData.result || !receiptData.result) { + return NextResponse.json({ error: "Transaction not found" }, { status: 404 }); + } + + // Get block information + const blockResponse = await fetch(ALCHEMY_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 3, + method: 'eth_getBlockByHash', + params: [txData.result.blockHash, false] + }) + }); + const blockData = await blockResponse.json(); + + // Convert hex values to decimal + const value = parseInt(txData.result.value, 16); + const gasPrice = parseInt(txData.result.gasPrice, 16); + const gasUsed = parseInt(receiptData.result.gasUsed, 16); + const gasLimit = parseInt(txData.result.gas, 16); + const timestamp = parseInt(blockData.result.timestamp, 16); + + // Format the transaction data + const transaction = { + hash: txData.result.hash, + from: txData.result.from, + to: txData.result.to, + value: value.toString(), + valueInEth: (value / 1e18).toFixed(6), + gasPrice: gasPrice.toString(), + gasLimit: gasLimit.toString(), + gasUsed: gasUsed.toString(), + nonce: parseInt(txData.result.nonce, 16), + status: receiptData.result.status === '0x1' ? "Success" : "Failed", + timestamp: timestamp, + blockNumber: parseInt(txData.result.blockNumber, 16), + blockHash: txData.result.blockHash, + confirmations: parseInt(receiptData.result.confirmations || '0', 16), + effectiveGasPrice: parseInt(receiptData.result.effectiveGasPrice, 16).toString(), + type: parseInt(txData.result.type, 16), + data: txData.result.input, + txFee: ((gasUsed * parseInt(receiptData.result.effectiveGasPrice, 16)) / 1e18).toFixed(6), + }; + + return NextResponse.json(transaction); + } catch (error) { + console.error("Error fetching transaction:", error); + return NextResponse.json( + { error: "Failed to fetch transaction details" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/block-txns/page.tsx b/app/block-txns/page.tsx new file mode 100644 index 0000000..fc73da0 --- /dev/null +++ b/app/block-txns/page.tsx @@ -0,0 +1,271 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSearchParams, useRouter } from "next/navigation"; +import { motion } from "framer-motion"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Loader2, ArrowLeft, + ChevronLeft, ChevronRight +} from "lucide-react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import ParticlesBackground from "@/components/ParticlesBackground"; + +interface Transaction { + blockHash: string; + blockNumber: string; + from: string; + gas: string; + gasPrice: string; + hash: string; + input: string; + nonce: string; + to: string; + transactionIndex: string; + value: string; + type: string; + timestamp: number; +} + +interface BlockData { + blockNumber: string; + timestamp: number; + transactions: Transaction[]; + total: number; +} + +const ITEMS_PER_PAGE = 20; + +export default function BlockTransactions() { + const searchParams = useSearchParams(); + const blockNumber = searchParams.get("number"); + const router = useRouter(); + + const [blockData, setBlockData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [page, setPage] = useState(1); + + useEffect(() => { + const fetchBlockData = async () => { + if (!blockNumber) { + setError("Block number is required"); + setLoading(false); + return; + } + + try { + const response = await fetch(`/api/alchemy-block-txns?blockNumber=${blockNumber}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Failed to fetch block data"); + } + + setBlockData(data); + setError(null); + } catch (err) { + console.error("Error fetching block data:", err); + setError(err instanceof Error ? err.message : "Failed to fetch block data"); + setBlockData(null); + } finally { + setLoading(false); + } + }; + + setLoading(true); + fetchBlockData(); + }, [blockNumber]); + + if (loading) { + return ( + <> + +
+ + + +

Loading Block Transactions...

+
+
+
+ + ); + } + + if (error || !blockData) { + return ( + <> + +
+ + +
+

{error || "Block data not found"}

+ +
+
+
+
+ + ); + } + + const totalTransactions = blockData.total; + const totalPages = Math.ceil(totalTransactions / ITEMS_PER_PAGE); + const currentPage = Math.min(Math.max(1, page), totalPages); + const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; + const endIndex = Math.min(startIndex + ITEMS_PER_PAGE, totalTransactions); + const paginatedTransactions = blockData.transactions.slice(startIndex, endIndex); + + return ( + <> + + +
+ +
+

+ Block #{blockData.blockNumber} + + {totalTransactions} Transactions + +

+

+ {new Date(blockData.timestamp * 1000).toLocaleString()} +

+
+
+ + + + {totalTransactions > 0 ? ( + <> +
+ + + + Tx Hash + From + To + Value (ETH) + Gas Price (Gwei) + Gas Limit + + + + {paginatedTransactions.map((tx) => ( + + + + + + + + + + + + {Number(tx.value).toFixed(12)} + + + {(Number(tx.gasPrice) / 1e9).toFixed(2)} + + + {Number(tx.gas).toLocaleString()} + + + ))} + +
+
+ + {totalPages > 1 && ( +
+

+ Showing {startIndex + 1}-{endIndex} of {totalTransactions} +

+
+ + + Page {currentPage} of {totalPages} + + +
+
+ )} + + ) : ( +
+ No transactions found in this block +
+ )} +
+
+
+ + ); +} \ No newline at end of file diff --git a/app/block/page.tsx b/app/block/page.tsx new file mode 100644 index 0000000..5a00745 --- /dev/null +++ b/app/block/page.tsx @@ -0,0 +1,253 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSearchParams, useRouter } from "next/navigation"; +import { motion } from "framer-motion"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Loader2, Copy, ExternalLink, + Clock, Hash, ChevronUp, + Cpu, Fuel, Pickaxe, + Database, Layers +} from "lucide-react"; +import { toast } from "sonner"; +import { format } from "date-fns"; +import { CircularProgressbar, buildStyles } from "react-circular-progressbar"; +import "react-circular-progressbar/dist/styles.css"; +import ParticlesBackground from "@/components/ParticlesBackground"; + +interface BlockDetails { + number: string; + hash: string; + parentHash: string; + timestamp: string; + nonce: string; + difficulty: string; + gasLimit: string; + gasUsed: string; + miner: string; + baseFeePerGas: string; + extraData: string; + transactions: string[]; + size: string; +} + +const InfoCard = ({ title, icon: Icon, children }: any) => ( +
+
+ +

{title}

+
+ {children} +
+); + +export default function BlockDetails() { + const searchParams = useSearchParams(); + const blockNumber = searchParams.get("number"); + const router = useRouter(); + const [block, setBlock] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchBlock = async () => { + if (!blockNumber) { + setError("Block number is required"); + setLoading(false); + return; + } + + try { + const response = await fetch(`/api/alchemy-block?blockNumber=${blockNumber}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Failed to fetch block details"); + } + + setBlock(data); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to fetch block details"); + } finally { + setLoading(false); + } + }; + + fetchBlock(); + }, [blockNumber]); + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + toast.success("Copied to clipboard!"); + }; + + if (loading) { + return ( +
+ + + +

Loading Block Details...

+
+
+
+ ); + } + + if (error || !block) { + return ( +
+ + +
+

{error || "Block not found"}

+
+
+
+
+ ); + } + + return ( + <> + + + + +
+ +

+ #{block.number} +

+
+ + +

+ {format(new Date(block.timestamp), "PPpp")} +

+
+ + +
+

+ {block.hash} +

+ +
+
+ + + + + + +
+ {/* Circular Progress Bar */} +
+ +
+ + {/* Gas Usage Details */} +
+

+ {Number(block.gasUsed).toLocaleString()} / {Number(block.gasLimit).toLocaleString()} + + ({((Number(block.gasUsed) / Number(block.gasLimit)) * 100).toFixed(2)}%) + +

+

+{((Number(block.gasUsed) / Number(block.gasLimit)) * 100 - 100).toFixed(2)}% Gas Target

+
+
+
+ + + + + + +

+ {(Number(block.baseFeePerGas) / 1e9).toFixed(2)} Gwei +

+
+ + +

+ {Number(block.size).toLocaleString()} bytes +

+
+
+ +
+

+ Transactions + + {block.transactions.length} + +

+
+
+ {block.transactions.slice(0, 9).map((hash) => ( + + ))} +
+ {block.transactions.length > 9 && ( +
+ +
+ )} +
+
+
+
+
+ + ); +} \ No newline at end of file diff --git a/app/token/page.tsx b/app/token/page.tsx new file mode 100644 index 0000000..fda2f57 --- /dev/null +++ b/app/token/page.tsx @@ -0,0 +1,388 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSearchParams, useRouter } from "next/navigation"; +import { motion } from "framer-motion"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import ParticlesBackground from "@/components/ParticlesBackground"; +import { + Loader2, Copy, ExternalLink, + Coins, Users, ArrowLeftRight, + Calendar, Shield, Clock, + TrendingUp, DollarSign, BarChart3, + ArrowUpRight, ArrowDownRight +} from "lucide-react"; +import { toast } from "sonner"; +import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "@/components/ui/table"; + +// Types +interface TokenTransfer { + hash: string; + from: string; + to: string; + value: string; + blockNumber: string; + timestamp: string; +} + +interface TokenDetails { + name: string; + symbol: string; + decimals: number; + address: string; + totalSupply: string; + logo: string; + holders: number; + transfers: number; + lastUpdated: string; + contractDeployed: string; + implementation?: string; + isProxy: boolean; + recentTransfers: TokenTransfer[]; + priceUSD?: string; + volume24h?: string; + marketCap?: string; +} + +// Helper Functions +const formatNumber = (num: number | string, useCommas = true) => { + if (!useCommas) return Number(num).toString(); + return new Intl.NumberFormat().format(Number(num)); + }; + +const formatUSD = (value: string) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }).format(Number(value)); +}; + +// Components +const TokenStatCard = ({ + icon: Icon, + title, + value, + delay +}: { + icon: any; + title: string; + value: React.ReactNode; + delay: number; +}) => ( + +
+ +

{title}

+
+

{value}

+
+); + +// Main Component +export default function TokenPage() { + const searchParams = useSearchParams(); + const address = searchParams.get("address"); + const [token, setToken] = useState(null); + const router = useRouter(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchToken = async () => { + if (!address) { + setError("Token address is required"); + setLoading(false); + return; + } + + try { + const response = await fetch(`/api/alchemy-token?address=${address}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Failed to fetch token details"); + } + + setToken(data); + } catch (err) { + console.error("Error fetching token:", err); + setError(err instanceof Error ? err.message : "Failed to fetch token details"); + } finally { + setLoading(false); + } + }; + + fetchToken(); + }, [address]); + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + toast.success("Copied to clipboard!"); + }; + + if (loading) { + return ( +
+ + + +

Loading Token Details...

+
+
+
+ ); + } + + if (error || !token) { + return ( +
+ + +
+

{error || "Token not found"}

+
+
+
+
+ ); + } + + return ( + <> + + + + + +
+
+ {token.name} { + e.currentTarget.src = '/placeholder-token.png'; + }} + /> + {token.priceUSD && ( + + ${Number(token.priceUSD).toFixed(2)} + + )} +
+
+ + {token.name} + + {token.symbol} + + +

+ {token.address} +

+
+
+
+ + +
+
+
+ + +
+ + {formatNumber(Number(token.totalSupply) / Math.pow(10, token.decimals))} + {token.symbol} + + } + delay={0.2} + /> + + + + {token.marketCap && ( + + )} + + {token.volume24h && ( + + )} + + +
+ + {token.recentTransfers && token.recentTransfers.length > 0 && ( + +

Recent Transfers

+
+ + + + Transaction + Block + Type + From + To + Amount + Time + + + + {token.recentTransfers.map((transfer) => ( + + + + + + + + + {transfer.from === token.address ? ( + + + Out + + ) : ( + + + In + + )} + + + + + + + + + + {transfer.from === token.address ? "-" : "+"} + + {/* {(Number(transfer.value) / Math.pow(10, token.decimals))} {token.symbol} */} + {transfer.value} {token.symbol} + {token.priceUSD && ( +
+ {formatUSD((Number(transfer.value) / Math.pow(10, token.decimals) * Number(token.priceUSD)).toString())} +
+ )} +
+ + {new Date(transfer.timestamp).toLocaleString()} + +
+ ))} +
+
+
+
+ )} +
+
+
+ + ); +} \ No newline at end of file diff --git a/app/txn-hash/page.tsx b/app/txn-hash/page.tsx new file mode 100644 index 0000000..ca5bea7 --- /dev/null +++ b/app/txn-hash/page.tsx @@ -0,0 +1,323 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSearchParams,useRouter } from "next/navigation"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Loader2, ExternalLink, Copy, XCircle } from "lucide-react"; +import { toast } from "sonner"; +import { format } from "date-fns"; +import { motion } from "framer-motion"; +import { ArrowRight, ArrowDownRight } from "lucide-react"; +import ParticlesBackground from "@/components/ParticlesBackground"; + +interface TransactionDetails { + hash: string; + from: string; + to: string; + value: string; + valueInEth: string; + gasPrice: string; + gasLimit: string; + gasUsed: string; + nonce: number; + status: string; + timestamp: number; + blockNumber: number; + blockHash: string; + confirmations: number; + effectiveGasPrice: string; + type: number; + data: string; + txFee: string; +} + +export default function TransactionDetails() { + const router = useRouter(); + const searchParams = useSearchParams(); + const hash = searchParams.get("hash"); + const [transaction, setTransaction] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const formatGwei = (wei: string) => { + return (Number(wei) / 1e9).toFixed(2) + " Gwei"; + }; + + useEffect(() => { + const fetchTransaction = async () => { + if (!hash) { + setError("Transaction hash is required"); + setLoading(false); + return; + } + + try { + const response = await fetch(`/api/alchemy-txnhash/?hash=${hash}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Failed to fetch transaction details"); + } + + setTransaction(data); + } catch (err) { + console.error("Error fetching transaction:", err); + setError(err instanceof Error ? err.message : "Failed to fetch transaction details"); + } finally { + setLoading(false); + } + }; + + fetchTransaction(); + }, [hash]); + + const getExplorerUrl = () => { + return `https://etherscan.io/tx/${hash}`; + }; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + toast.success("Copied to clipboard!"); + }; + + if (loading) { + return ( + + + + +

Loading Transaction Details...

+
+
+
+ ); + } + + if (error) { + return ( + + +
+ +

Transaction Error

+

{error}

+
+
+
+ ); + } + + if (!transaction) return null; + + return ( + <> + + + + + + + + Transaction Details + + + {transaction.status} + + +
+ + +
+
+
+ +
+ + {/* Transaction Basic Info */} +
+
+

Transaction Hash

+
+

{transaction.hash}

+
+ + {/* From/To Section */} +
+
+ +

From

+
+ +
+ +
+
+ +

To

+
+ +
+ + {/* Value Section */} +
+

Value

+

+ {transaction.valueInEth} ETH + + (${(Number(transaction.valueInEth) * 2000).toFixed(4)}) + +

+
+
+ + + {/* Gas Information */} +
+

Gas Information

+
+

+ Gas Price: + {formatGwei(transaction.gasPrice)} +

+

+ Gas Limit: + {transaction.gasLimit} +

+

+ Gas Used: + + {transaction.gasUsed} ({(Number(transaction.gasUsed) / Number(transaction.gasLimit) * 100).toFixed(2)}%) + +

+

+ Effective Gas Price: + {formatGwei(transaction.effectiveGasPrice)} +

+

+ Transaction Fee: + {transaction.txFee+"ETH"} +

+
+
+ + {/* Block Information */} +
+

Block Information

+
+

+ Block Number: + #{transaction.blockNumber} +

+

+ Block Hash: + {transaction.blockHash} +

+

+ Confirmations: + {transaction.confirmations} +

+

+ Timestamp: + + {format(new Date(transaction.timestamp * 1000), "PPpp")} + +

+
+
+ + {/* Transaction Details */} +
+

Transaction Details

+
+

+ Nonce: + {transaction.nonce} +

+

+ Type: + {transaction.type} +

+
+
+
+
+ + {/* Transaction Data Section */} + +

Input Data

+
+
+              {transaction.data}
+            
+
+
+
+
+
+ + ); +} \ No newline at end of file diff --git a/components/NFT/AnimatedNFTCard.tsx b/components/NFT/AnimatedNFTCard.tsx index a48e841..688d03f 100644 --- a/components/NFT/AnimatedNFTCard.tsx +++ b/components/NFT/AnimatedNFTCard.tsx @@ -4,6 +4,8 @@ import { ExternalLink } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { getChainColorTheme } from '@/lib/api/chainProviders'; import LazyImage from './LazyImage'; +import { ipfsUriToGatewayUrl } from '@/lib/utils/ipfsUtils'; +import Image from "next/legacy/image"; interface NFT { id: string; @@ -30,6 +32,9 @@ export default function AnimatedNFTCard({ nft, onClick, index = 0, isVirtualized const [imageLoaded, setImageLoaded] = useState(false); const cardRef = useRef(null); + // Process image URL for IPFS compatibility + const imageUrl = nft.imageUrl ? ipfsUriToGatewayUrl(nft.imageUrl) : ''; + // Chain-specific styling const chainTheme = getChainColorTheme(nft.chain); @@ -216,13 +221,14 @@ export default function AnimatedNFTCard({ nft, onClick, index = 0, isVirtualized {/* Network Badge - Positioned absolutely top-right */}
-
- + {networkBadge.name}
@@ -235,11 +241,11 @@ export default function AnimatedNFTCard({ nft, onClick, index = 0, isVirtualized {/* NFT Image with progressive loading */}
setImageLoaded(true)} onError={() => {/* Error handled inside LazyImage */}} @@ -264,16 +270,36 @@ export default function AnimatedNFTCard({ nft, onClick, index = 0, isVirtualized {/* Attributes */}
- {nft.attributes?.slice(0, 3).map((attr, i) => ( - - {attr.trait_type === 'Network' ? null : `${attr.trait_type}: ${attr.value}`} - - ))} + {(() => { + const processAttributes = () => { + let attrs = nft.attributes; + if (attrs && !Array.isArray(attrs) && typeof attrs === 'object') { + attrs = Object.entries(attrs).map(([trait_type, value]) => ({ + trait_type, + value: String(value) + })); + } + return (Array.isArray(attrs) ? attrs : []) + .filter(attr => + attr && + typeof attr === 'object' && + 'trait_type' in attr && + 'value' in attr + ) + .slice(0, 3); + }; + + return processAttributes().map((attr, i) => ( + + {attr.trait_type === 'Network' ? null : `${attr.trait_type}: ${attr.value}`} + + )); + })()}
diff --git a/components/NFT/CollectionCard3D.tsx b/components/NFT/CollectionCard3D.tsx index b95d9f2..9cf1c51 100644 --- a/components/NFT/CollectionCard3D.tsx +++ b/components/NFT/CollectionCard3D.tsx @@ -1,5 +1,5 @@ import { useRef, useState } from 'react'; -import Image from 'next/image'; +import Image from 'next/legacy/image'; import Link from 'next/link'; import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion'; import { Sparkles, Verified, Users, ExternalLink, ChevronRight } from 'lucide-react'; @@ -23,7 +23,7 @@ interface CollectionCardProps { onClick?: () => void; } -export default function CollectionCard3D({ collection, index, onClick }: CollectionCardProps) { +export default function CollectionCard3D({ collection, index = 0, onClick }: CollectionCardProps) { const cardRef = useRef(null); const [isHovered, setIsHovered] = useState(false); const [imgLoaded, setImgLoaded] = useState(false); @@ -114,17 +114,49 @@ export default function CollectionCard3D({ collection, index, onClick }: Collect }, }; - // Get network name - const getNetworkName = () => { - const networks: Record = { - '0x1': 'Ethereum', - '0xaa36a7': 'Sepolia', - '0x38': 'BNB Chain', - '0x61': 'BNB Testnet' - }; - return networks[collection.chain] || 'Unknown Network'; + // Get network badge details + const getNetworkBadge = () => { + switch (collection.chain) { + case '0x1': + return { + icon: '/icons/eth.svg', + name: 'ETH', + bgClass: 'bg-blue-500/20', + textColor: '#6b8df7' + }; + case '0xaa36a7': + return { + icon: '/icons/eth.svg', + name: 'Sepolia', + bgClass: 'bg-blue-400/20', + textColor: '#8aa2f2' + }; + case '0x38': + return { + icon: '/icons/bnb.svg', + name: 'BNB', + bgClass: 'bg-yellow-500/20', + textColor: '#F0B90B' + }; + case '0x61': + return { + icon: '/icons/bnb.svg', + name: 'BNB Testnet', + bgClass: 'bg-yellow-400/20', + textColor: '#F5CA3B' + }; + default: + return { + icon: '/icons/eth.svg', + name: 'ETH', + bgClass: 'bg-blue-500/20', + textColor: '#6b8df7' + }; + } }; + const networkBadge = getNetworkBadge(); + return ( - -
+
+
+ {/* Fix: Update the icon container to ensure proper display */} +
Chain - {getNetworkName()}
- + + {networkBadge.name} + +
@@ -221,7 +253,7 @@ export default function CollectionCard3D({ collection, index, onClick }: Collect {collection.name} diff --git a/components/NFT/LazyImage.tsx b/components/NFT/LazyImage.tsx index ee1259b..78ec9a3 100644 --- a/components/NFT/LazyImage.tsx +++ b/components/NFT/LazyImage.tsx @@ -1,160 +1,73 @@ -import { useState, useEffect, useRef } from 'react'; -import { motion } from 'framer-motion'; -import Image from 'next/image'; +import { useState, useEffect } from 'react'; +import Image, { ImageProps } from 'next/legacy/image'; import { Info } from 'lucide-react'; -interface LazyImageProps { +interface LazyImageProps extends Omit { src: string; - alt: string; - className?: string; - width?: number; - height?: number; - priority?: boolean; - fill?: boolean; - objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'; - placeholder?: 'blur' | 'empty'; - blurDataURL?: string; - sizes?: string; - quality?: number; - onLoad?: () => void; - onError?: () => void; + showLoadingIndicator?: boolean; + objectFit?: "fill" | "contain" | "cover" | "none" | "scale-down"; } -export default function LazyImage({ - src, - alt, - className = '', - width, - height, - priority = false, - fill = false, - objectFit = 'cover', - placeholder = 'empty', - blurDataURL, - sizes = '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw', - quality = 75, - onLoad, +export default function LazyImage({ + src, + alt, + showLoadingIndicator = false, onError, + onLoad, + objectFit = "cover", + ...props }: LazyImageProps) { - const [isLoaded, setIsLoaded] = useState(false); - const [isInView, setIsInView] = useState(false); - const [imgSrc, setImgSrc] = useState(null); + const [imgSrc, setImgSrc] = useState(src); + const [loading, setLoading] = useState(true); const [error, setError] = useState(false); - const imgRef = useRef(null); - const placeholderColors = useRef([ - 'rgb(30, 30, 30)', 'rgb(40, 40, 40)', 'rgb(50, 50, 50)', 'rgb(35, 35, 35)' - ]); - - // Function to transform IPFS URLs - const transformUrl = (url: string): string => { - if (!url) return ''; - if (url.startsWith('ipfs://')) { - return `https://ipfs.io/ipfs/${url.slice(7)}`; - } - return url; - }; - - // Set up intersection observer to detect when image is in viewport - useEffect(() => { - if (!imgRef.current || priority) { - setIsInView(true); - return; - } - - const observer = new IntersectionObserver( - ([entry]) => { - setIsInView(entry.isIntersecting); - }, - { - root: null, - rootMargin: '200px', // Load images 200px before they appear in viewport - threshold: 0.01, - } - ); - - observer.observe(imgRef.current); - - return () => { - if (imgRef.current) { - observer.unobserve(imgRef.current); - } - }; - }, [priority]); - - // Set image source when in view + useEffect(() => { - if (isInView && src) { - setImgSrc(transformUrl(src)); - } - }, [isInView, src]); - - // Handle image load - const handleImageLoad = () => { - setIsLoaded(true); - if (onLoad) onLoad(); - }; - - // Handle image error - const handleImageError = () => { + setImgSrc(src); + setLoading(true); + setError(false); + }, [src]); + + const handleError = () => { + setImgSrc('/images/placeholder-nft.png'); // Fallback image setError(true); - setIsLoaded(true); - if (onError) onError(); + setLoading(false); + // Fix: Don't pass Error object directly to onError + if (onError) onError({} as React.SyntheticEvent); + }; + + const handleLoad = (event: any) => { + setLoading(false); + if (onLoad) onLoad(event); }; - - // Generate random placeholder background - const placeholderBackground = `linear-gradient(45deg, ${placeholderColors.current[0]}, ${placeholderColors.current[1]}, ${placeholderColors.current[2]}, ${placeholderColors.current[3]})`; + + // Handle IPFS and data URLs properly + // Fix: Changed from let to const + const finalSrc = imgSrc; + + if (!imgSrc || error) { + return ( +
+ +
+ ); + } return ( -
- {/* Placeholder with shimmer effect */} - {!isLoaded && ( - - )} - - {/* Main image */} - {imgSrc && isInView && ( - {alt} - )} - - {/* Error fallback */} - {error && ( + <> + {loading && showLoadingIndicator && (
- +
)} -
+ + {alt + ); } diff --git a/components/NFT/NetworkSelector.tsx b/components/NFT/NetworkSelector.tsx index 28be75c..7a2c390 100644 --- a/components/NFT/NetworkSelector.tsx +++ b/components/NFT/NetworkSelector.tsx @@ -33,14 +33,6 @@ const networks: Network[] = [ color: 'bg-blue-500/20 border-blue-500/50', hexColor: '#6b8df7' }, - { - id: '0xaa36a7', - name: 'Sepolia', - icon: '/icons/eth.svg', - color: 'bg-blue-400/20 border-blue-400/50', - hexColor: '#8aa2f2', - testnet: true - }, { id: '0x38', name: 'BNB Chain', diff --git a/components/search-offchain/SearchBarOffChain.tsx b/components/search-offchain/SearchBarOffChain.tsx index 4f0315e..f0e4abd 100644 --- a/components/search-offchain/SearchBarOffChain.tsx +++ b/components/search-offchain/SearchBarOffChain.tsx @@ -4,7 +4,7 @@ import { useState } from "react" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { useRouter } from "next/navigation" -import { Search, X, Globe, AlertTriangle } from "lucide-react" +import { Search, X, Globe, AlertTriangle, Database, Hash, Coins, Layers } from "lucide-react" import { LoadingScreen } from "@/components/loading-screen" import Neo4jIcon from "@/components/icons/Neo4jIcon" import { @@ -20,7 +20,7 @@ import { toast } from "sonner" export type NetworkType = "mainnet" | "optimism" | "arbitrum" export type ProviderType = "etherscan" | "infura" - +type InputType = "ADDRESS" | "TRANSACTION-HASH" | "TOKEN" | "BLOCK" | "NEO4J" | "UNKNOWN"; // Ethereum address validation regex pattern const ETH_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/; @@ -30,10 +30,66 @@ export default function SearchBar() { const [addressError, setAddressError] = useState(null) const router = useRouter() - const [searchType, setSearchType] = useState<"onchain" | "offchain">("offchain") + const [searchType, setSearchType] = useState<"onchain" | "offchain"| "Txn Hash" | "Token" | "Block" | "All">("offchain") const [network, setNetwork] = useState("mainnet") const [provider, setProvider] = useState("etherscan") + const detectInputType = (input: string): InputType => { + // Clean the input + const cleanInput = input.trim().toLowerCase(); + + // Check for empty input + if (!cleanInput) return "UNKNOWN"; + + // Ethereum Address and Token (0x followed by 40 hex characters) + if (/^0x[a-f0-9]{40}$/.test(cleanInput)) { + return "ADDRESS"; + } + + // Transaction Hash (0x followed by 64 hex characters) + if (/^0x[a-f0-9]{64}$/.test(cleanInput)) { + return "TRANSACTION-HASH"; + } + + // Block Number (numeric only) + if (/^\d+$/.test(cleanInput)) { + return "BLOCK"; + } + + // Neo4j identifier (at least 3 characters) + if (/^0x[a-f0-9]{40}$/.test(cleanInput)) { + return "NEO4J"; + } + + return "UNKNOWN"; + }; + + const handleUniversalSearch = async (input: string) => { + const inputType = detectInputType(input); + + switch (inputType) { + case "ADDRESS": + // Check if it's a token contract + const isToken = false; // You would need to implement token detection logic here + if (isToken) { + return `/token/?address=${encodeURIComponent(input)}`; + } + return `/search/?address=${encodeURIComponent(input)}&network=mainnet&provider=etherscan`; + + case "TRANSACTION-HASH": + return `/txn-hash/?hash=${encodeURIComponent(input)}`; + + case "BLOCK": + return `/block/?number=${encodeURIComponent(input)}`; + + case "NEO4J": + return `/search-offchain/?address=${encodeURIComponent(input)}`; + + default: + throw new Error("Unable to determine search type"); + } + }; + // Validate Ethereum address const validateAddress = (addr: string): boolean => { if (!addr) return false; @@ -44,9 +100,32 @@ export default function SearchBar() { setAddressError("Invalid Ethereum address format. Must start with 0x followed by 40 hex characters."); return false; } - } else { + } else if(searchType === "Txn Hash"){ + if(addr.length !== 66){ + setAddressError("Invalid Transaction Hash format. Must be 66 characters long."); + return false; + } + }else if(searchType === "Token"){ + if(addr.length !== 42){ + setAddressError("Invalid Token address format. Must be 42 characters long."); + return false; + } + }else if (searchType === "Block"){ + if(addr.length < 1){ + setAddressError("Invalid Block number format. Must be at least 1 character long."); + return false; + } + }else if(searchType === "All"){ + // Detect logic to search for all types + const inputType = detectInputType(addr); + if (inputType === "UNKNOWN") { + setAddressError("Invalid search input. Please enter a valid address, transaction hash, token address, or block number."); + return false; + } + } + else { // For off-chain searches, validate Neo4j ID format - if (addr.length < 3) { + if (!ETH_ADDRESS_REGEX.test(addr)) { setAddressError("Neo4j identifier must be at least 3 characters"); return false; } @@ -109,7 +188,17 @@ export default function SearchBar() { await new Promise(resolve => setTimeout(resolve, 1000)) if (searchType === "onchain") { router.push(`/search/?address=${encodeURIComponent(address)}&network=${network}&provider=${provider}`) - } else { + } else if(searchType === "Txn Hash"){ + router.push(`/txn-hash/?hash=${encodeURIComponent(address)}`) + } else if(searchType === "Token"){ + router.push(`/token/?address=${encodeURIComponent(address)}`) + } else if(searchType === "Block"){ + router.push(`/block/?number=${encodeURIComponent(address)}`) + }else if(searchType === "All"){ + // Detect logic to search for all types + const route = await handleUniversalSearch(address); + router.push(route); + }else { router.push(`/search-offchain/?address=${encodeURIComponent(address)}`) } } catch (error) { @@ -147,15 +236,35 @@ export default function SearchBar() { @@ -181,20 +290,40 @@ export default function SearchBar() {
@@ -239,19 +368,48 @@ export default function SearchBar() { - ) : ( + ) : searchType === "offchain" ?(
Neo4j Graph Database
- )} + ): searchType === "Txn Hash" ? ( +
+ + Transaction Explorer +
+ ) : searchType === "Token" ? ( +
+ + Token Explorer +
+ ) : searchType === "Block" ? ( +
+ + Block Explorer +
+ ) : ( +
+ + Universal Search +
+ ) + }