From 8f35a38e82769af2df7f9099355c17f584c60712 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Wed, 26 Mar 2025 12:06:50 +0700 Subject: [PATCH 01/10] Update NFT collection data in alchemyNFTApi.ts --- lib/api/alchemyNFTApi.ts | 56 ++++++++++++---------------------------- 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/lib/api/alchemyNFTApi.ts b/lib/api/alchemyNFTApi.ts index b4e8ed7..82b4683 100644 --- a/lib/api/alchemyNFTApi.ts +++ b/lib/api/alchemyNFTApi.ts @@ -445,52 +445,28 @@ const mockBNBCollections = [ category: 'Gaming' }, { - id: '0xcec33930ba196cdf9f38e1a5e2a1e0708450d20f', - name: 'Era7: Game of Truth', - description: 'Era7: Game of Truth is a play-to-earn trading card game inspired by Magic: The Gathering, Hearthstone, and Runeterra. Build your deck and battle against players around the world!', - imageUrl: 'https://images.pocketgamer.biz/T/52539/2022/52539_Era7_Game_of_Truth_screenshot_1.png', - bannerImageUrl: 'https://pbs.twimg.com/profile_banners/1428004235405357057/1648021562/1500x500', - floorPrice: '0.4', - totalSupply: '33000', + id: '0xDf7952B35f24aCF7fC0487D01c8d5690a60DBa07', + name: 'Pancake Bunnies', + description: 'Pancake Bunnies are collectible NFTs on BNB Chain created by PancakeSwap. These adorable bunny NFTs feature various themes and styles, serving as digital collectibles in the PancakeSwap ecosystem.', + imageUrl: 'https://dappradar.com/image-resizer/width=64,quality=100/https://dashboard-assets.dappradar.com/document/4600/pancakeswap-dapp-defi-bsc-logo-166x166_783df04863220a6371e78725e2fa3320.png', + bannerImageUrl: 'https://dappradar.com/nft-metadata-image?encrypted=true&format=big-preview&filePath=c6fb236ebe75b44224d832afe92b7bf4d9c3bbd3083f2f7e8e3165b4585f90e080a2aaf821da426f31d3dc58a870ae477bd8f1f4c95f97fd75ff78fd17418836ad28289b8e3dde0f2b5d46a418ce93978d27239f31c23aa09b9b4ab80cb1481539fdb6c65858854966251d5dd8ec613705cb9fd4b06ade913edc20a5eb5b0bdf54eb148e3df70d5bf9eb9732ed301921', + floorPrice: '0.2', + totalSupply: '14000', chain: '0x38', verified: true, - category: 'Gaming' - }, - { - id: '0x04a5e3ed4907b781f702adbddf1b7e771c31b0f2', - name: 'BSC Punks', - description: 'CryptoPunks on BSC - 10,000 uniquely generated characters on the BNB Chain.', - imageUrl: 'https://lh3.googleusercontent.com/BdxvLseXcfl57BiuQcQYdJ64v-aI8din7WPk0Pgo3qQFhAUH-B6i-dCqqc_mCkRIzULmwzwecnohLhrcH8A9mpWIZqA7ygc52Sr81hE=s130', - bannerImageUrl: 'https://pbs.twimg.com/profile_banners/1364731917736632321/1649351522/1500x500', - floorPrice: '0.25', - totalSupply: '10000', - chain: '0x38', - verified: false, - category: 'Art & Collectibles' - }, - { - id: '0x85f0e02cb992aa1f9f47112f815f519ef1a59e2d', - name: 'BNB Bulls Club', - description: 'The BNB Bulls Club is a collection of 10,000 unique NFTs living on the BNB Chain, each representing membership to the club with unique utilities.', - imageUrl: 'https://static-nft.pancakeswap.com/mainnet/0x85F0e02cb992aa1F9F47112F815F519EF1A59E2D/banner-lg.png', - bannerImageUrl: 'https://static-nft.pancakeswap.com/mainnet/0x85F0e02cb992aa1F9F47112F815F519EF1A59E2D/banner-lg.png', - floorPrice: '1.2', - totalSupply: '10000', - chain: '0x38', - verified: false, - category: 'Membership' + category: 'Collectibles' }, { - id: '0xc274a97f1691ef390f662067e95a6eff1f99b504', - name: 'Tin Fun NFT', - description: 'Buildspace: Build your own DAO with Javascript | Cohort Alkes | #360 - DAOs are taking over. Build one yourself for fun. Maybe it\'s a meme DAO for your friends. Maybe it\'s a DAO that aims to fix climate change. Up to you. We\'ll be going over things like minting a membership NFT, creating/airdropping a token, public treasuries, and governance using a token!', - imageUrl: 'https://i.seadn.io/s/raw/files/a531bedf317b5ffe5a35d559b5c94cd9.jpg?auto=format&dpr=1&w=256', - bannerImageUrl: 'https://i.seadn.io/s/primary-drops/0xc274a97f1691ef390f662067e95a6eff1f99b504/31341974:about:media:98e2f8a2-a8aa-46d9-9267-87108353c759.jpeg?auto=format&dpr=1&w=1920', - floorPrice: '0.0929', - totalSupply: '9999', + id: '0x9F471abCddc810E561873b35b8aad7d78e21a48e', + name: 'Galxe OAT', + description: 'Galxe OAT (On-chain Achievement Token) is a collection of NFTs that represent achievements and credentials earned on Galxe, a Web3 credential data network. Each OAT signifies participation in ecosystem campaigns and community events.', + imageUrl: 'https://i.seadn.io/gcs/files/c083274101cf84f66c7490b14d7dc480.png?auto=format&dpr=1&w=256', + bannerImageUrl: 'https://i.seadn.io/gcs/files/c083274101cf84f66c7490b14d7dc480.png?auto=format&dpr=1&w=256', + floorPrice: '0.05', + totalSupply: '250000', chain: '0x38', verified: true, - category: 'China' + category: 'Credentials' } ]; From 1f55f1baee15d1cc654e1c12c2e94cf58055acd9 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Wed, 26 Mar 2025 12:13:36 +0700 Subject: [PATCH 02/10] Refactor attribute processing in AnimatedNFTCard to handle non-array attributes --- components/NFT/AnimatedNFTCard.tsx | 40 ++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/components/NFT/AnimatedNFTCard.tsx b/components/NFT/AnimatedNFTCard.tsx index a48e841..e76f182 100644 --- a/components/NFT/AnimatedNFTCard.tsx +++ b/components/NFT/AnimatedNFTCard.tsx @@ -264,16 +264,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}`} + + )); + })()}
From 25219590a606e30fae56716948cccebf1a6db315 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Wed, 26 Mar 2025 13:46:51 +0700 Subject: [PATCH 03/10] Add new NFT collections to alchemyNFTApi mock data --- lib/api/alchemyNFTApi.ts | 72 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/lib/api/alchemyNFTApi.ts b/lib/api/alchemyNFTApi.ts index 82b4683..b305813 100644 --- a/lib/api/alchemyNFTApi.ts +++ b/lib/api/alchemyNFTApi.ts @@ -444,6 +444,54 @@ const mockBNBCollections = [ verified: true, category: 'Gaming' }, + { + id: '0x25dc4d9e2598c21dc020aa7b741377ecde971c2f', + name: 'RhnoX', + description: 'RhnoX, dinooo', + imageUrl: 'https://img-cdn.magiceden.dev/rs:fill:400:0:0/plain/https%3A%2F%2Fbafybeihxzn2o4daow6cbth7pzlwwsdhb3ryzzvhok4ukz3uiodyygexcmu.ipfs.w3s.link%2F7cbfe2c138dd4140bd5c6739dae1a346_400x400.webp', + bannerImageUrl: 'https://img-cdn.magiceden.dev/da:t/rs:fill:400:0:0/plain/https%3A%2F%2Fimg.reservoir.tools%2Fimages%2Fv2%2Fbsc%2Fi9YO%252F4yHXUdJsWcTqhqvf32nwo61iwc9Eyq2MC1NyBZeThvD0pm%252F1LLHrhwGoSCwdW5QSWIe85nAF4iiibr1aGhRpa5Hg0%252BO5wPuwlsxeqo%253D.png', + floorPrice: '0', + totalSupply: '10000', + chain: '0x38', + verified: true, + category: 'Animals' + }, + { + id: '0x6dfbb01ecb7991366cd8acc4d18dcc67bbe345ba', + name: 'Reefer by CoralApp', + description: 'Reefer by CoralApp, horse fishh', + imageUrl: 'https://img-cdn.magiceden.dev/rs:fill:400:0:0/plain/https%3A%2F%2Fimg.reservoir.tools%2Fimages%2Fv2%2Fbsc%2FM%252FK47YBCAs6ieLgkYDuIwv46DpQnevL2hmBULPkK3BuBY8BG4GGzXB22H3E0Phb8M9RMHorMwBwK9g1GNRSmHqKVgKxZwKqawUsxLVBU98czgEyHu8JPigR%252B0hTOIEKPQ2fByOR1AiDomIAfUK%252BOjwn70fv7FQbGrZSFpOxFo3IM2D6K7unSJoB%252BIKRyIT38%252Fnx%252F0gpUvVgL3ehrSXBVchEdeTxrzHqQXzaxM9WErHlgdPAGFDVnnuDu62qDy0dJ', + bannerImageUrl: 'https://img-cdn.magiceden.dev/da:t/rs:fill:400:0:0/plain/https%3A%2F%2Fimg.reservoir.tools%2Fimages%2Fv2%2Fbsc%2Fi9YO%252F4yHXUdJsWcTqhqvfzU%252F3WKezZwCXanKqYOzVPg%252BaktrSCM23x2y61NuH7tbBMKNqglfI%252B%252Ba3fQp0Y8y2rfX9lGSU5olT3da4GRyRONg6tDeCs5Q29nd1XKfAxFv.png', + floorPrice: '0.2', + totalSupply: 'xxx', + chain: '0x38', + verified: true, + category: 'Animals' + }, + { + id: '0xabd1780208a62b9cbf9d3b7a1617918d42493933', + name: 'BitLife', + description: 'Reefer by CoralApp, horse fishh', + imageUrl: 'https://img-cdn.magiceden.dev/rs:fill:400:0:0/plain/https%3A%2F%2Fimg.cellula.life%2Froleimagefactory%2Fb108_B_5.gif', + bannerImageUrl: 'https://img-cdn.magiceden.dev/autoquality:size:1024000:20:80/da:t/rs:fill:400:0:0/plain/https%3A%2F%2Fimg.cellula.life%2Froleimagefactory%2Fa90_A_4.gif', + floorPrice: '0.0015', + totalSupply: 'xxx', + chain: '0x38', + verified: true, + category: 'Robots' + }, + { + id: '0xbd19b4c3c1e745e982f4d7f8bdf983d407e68a46', + name: 'AneeMate Genesis Zero', + description: 'Reefer by CoralApp, horse fishh', + imageUrl: 'https://img-cdn.magiceden.dev/rs:fill:400:0:0/plain/https%3A%2F%2Fimg.reservoir.tools%2Fimages%2Fv2%2Fbsc%2FCkMT33f1Sv9wfP6vXxX9omNeQgD5%252Bce5kPfKyaki6jmvvnHsubJ%252BWJr2NEaHrLZae3jIQM8an2M07TnJEan8HlV14iWeesdefnM%252BxMdKsgs%253D', + bannerImageUrl: 'https://img-cdn.magiceden.dev/da:t/rs:fill:64:0:0/plain/https%3A%2F%2Fmetadata.qorpo.world%2Faneemate-genesis%2Fmedia%2FGENESIS_PYROS.jpg', + floorPrice: '1.5', + totalSupply: '3000', + chain: '0x38', + verified: true, + category: 'Game' + }, { id: '0xDf7952B35f24aCF7fC0487D01c8d5690a60DBa07', name: 'Pancake Bunnies', @@ -456,6 +504,30 @@ const mockBNBCollections = [ verified: true, category: 'Collectibles' }, + { + id: '0xF250bf5953B601E42E93226F7A9e4e8B9E7435Af', + name: 'Legendary Pandra King', + description: 'Legendary Pandra King', + imageUrl: 'https://dappradar.com/image-resizer/width=64,quality=100/https://metadata.dappradar.com/api/media/collection/legendary-pandra-king-3?isThumbnail=0', + bannerImageUrl: 'https://dappradar.com/nft-metadata-image?encrypted=true&format=big-preview&filePath=c6fb236ebe75b44224d832afe92b7bf4d9c3bbd3083f2f7e8e3165b4585f90e010c8f5ab67e87091eb47c157f82c1054ed6056b2afd9d302705b406e5f48bfdd964ad5b88e9cc7fd9ec5aa8e626ef389eac6467952d11912c205003f262fe0550ccd94fa49444f38881ddfe418464db8129356323e33097fcccb9ccc0bf50793bbb4ba1ed84d7da3620008ad3527226d', + floorPrice: '0.2', + totalSupply: '135', + chain: '0x38', + verified: false, + category: 'Collectibles' + }, + { + id: '0xa9E9A78fF1027dc0dd1Ee54D7f134f191541Fe07', + name: 'Frontera', + description: 'Frontera', + imageUrl: 'https://dappradar.com/image-resizer/width=64,quality=100/https://metadata.dappradar.com/api/media/sales/any/nft-metadata/bsc/0xa9e9a78ff1027dc0dd1ee54d7f134f191541fe07/0xa9e9a78ff1027dc0dd1ee54d7f134f191541fe07_64_64.jpg', + bannerImageUrl: 'https://dappradar.com/nft-metadata-image?encrypted=true&format=big-preview&filePath=c6fb236ebe75b44224d832afe92b7bf4d9c3bbd3083f2f7e8e3165b4585f90e010c8f5ab67e87091eb47c157f82c1054ed6056b2afd9d302705b406e5f48bfdd964ad5b88e9cc7fd9ec5aa8e626ef389eac6467952d11912c205003f262fe0550ccd94fa49444f38881ddfe418464db8129356323e33097fcccb9ccc0bf50793bbb4ba1ed84d7da3620008ad3527226d', + floorPrice: '0.2', + totalSupply: '3200', + chain: '0x38', + verified: false, + category: 'Collectibles' + }, { id: '0x9F471abCddc810E561873b35b8aad7d78e21a48e', name: 'Galxe OAT', From d2b556fae944b7bca8c9645780679065501ba18d Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Wed, 26 Mar 2025 14:00:35 +0700 Subject: [PATCH 04/10] Remove Sepolia network references and update network handling logic --- app/NFT/collection/[collectionId]/page.tsx | 3 - app/NFT/collection/page.tsx | 15 +++- components/NFT/NetworkSelector.tsx | 8 -- lib/api/chainProviders.ts | 94 +++++++++------------- 4 files changed, 49 insertions(+), 71 deletions(-) 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..bca7892 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 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/lib/api/chainProviders.ts b/lib/api/chainProviders.ts index 0cf32cc..4b5de55 100644 --- a/lib/api/chainProviders.ts +++ b/lib/api/chainProviders.ts @@ -39,20 +39,6 @@ export const chainConfigs: Record = { }, testnet: false, }, - // Sepolia Testnet - '0xaa36a7': { - id: '0xaa36a7', - name: 'Sepolia', - rpcUrl: 'https://eth-sepolia.g.alchemy.com/v2/demo', - symbol: 'ETH', - blockExplorerUrl: 'https://sepolia.etherscan.io', - nativeCurrency: { - name: 'Sepolia Ether', - symbol: 'ETH', - decimals: 18 - }, - testnet: true, - }, // BNB Chain Mainnet '0x38': { id: '0x38', @@ -104,13 +90,12 @@ export const getChainProvider = (chainId: string) => { * @returns The full explorer URL */ export function getExplorerUrl(chainId: string, path: string = '', type: 'address' | 'tx' | 'token' | 'block' = 'address'): string { - const config = chainConfigs[chainId]; - if (!config) { - // Default to Ethereum mainnet if chain not found - return `https://etherscan.io/${type}/${path}`; - } - - return `${config.blockExplorerUrl}/${type}/${path}`; + const explorers: Record = { + '0x1': 'https://etherscan.io', + '0x38': 'https://bscscan.com', + '0x61': 'https://testnet.bscscan.com' + }; + return `${explorers[chainId] || 'https://etherscan.io'}/${type}/${path}`; } /** @@ -129,40 +114,33 @@ export const formatAddress = (address: string) => { * @returns Object with color values and utility classes */ export const getChainColorTheme = (chainId: string): ChainTheme => { - switch (chainId) { - case '0x1': - case '0xaa36a7': - return { - primary: '#6b8df7', - secondary: '#3b5ff7', - accent: 'blue', - light: '#d0d8ff', - backgroundClass: 'bg-blue-500/20', - borderClass: 'border-blue-500/50', - textClass: 'text-blue-400' - }; - case '0x38': - case '0x61': - return { - primary: '#F0B90B', - secondary: '#E6A50A', - accent: 'yellow', - light: '#FFF4D0', - backgroundClass: 'bg-yellow-500/20', - borderClass: 'border-yellow-500/50', - textClass: 'text-yellow-500' - }; - default: - return { - primary: '#6b8df7', - secondary: '#3b5ff7', - accent: 'blue', - light: '#d0d8ff', - backgroundClass: 'bg-blue-500/20', - borderClass: 'border-blue-500/50', - textClass: 'text-blue-400' - }; + // Default theme (Ethereum) + let theme = { + primary: '#6b8df7', + secondary: '#3b5cf5', + accent: '#4b6ef5', // Added missing property + light: '#d4ddff', // Added missing property + backgroundClass: 'bg-blue-900/20', + borderClass: 'border-blue-500/30', + textClass: 'text-blue-400', + buttonClass: 'bg-blue-600 hover:bg-blue-700' + }; + + // BNB Chain theme + if (chainId === '0x38' || chainId === '0x61') { + theme = { + primary: '#F0B90B', + secondary: '#F8D12F', + accent: '#EDAA00', // Added missing property + light: '#FFF3D3', // Added missing property + backgroundClass: 'bg-yellow-900/20', + borderClass: 'border-yellow-500/30', + textClass: 'text-yellow-400', + buttonClass: 'bg-yellow-600 hover:bg-yellow-700' + }; } + + return theme; }; /** @@ -171,8 +149,12 @@ export const getChainColorTheme = (chainId: string): ChainTheme => { * @returns Human-readable network name */ export const getNetworkName = (chainId: string): string => { - const config = chainConfigs[chainId]; - return config?.name || 'Unknown Network'; + const networks: Record = { + '0x1': 'Ethereum', + '0x38': 'BNB Chain', + '0x61': 'BNB Testnet' + }; + return networks[chainId] || 'Unknown Network'; }; /** From ba3481ca1421578f51184d48a8658406238e70a5 Mon Sep 17 00:00:00 2001 From: Mordred <95609626+TTMordred@users.noreply.github.com> Date: Wed, 26 Mar 2025 14:17:34 +0700 Subject: [PATCH 05/10] Add IPFS utility functions and integrate IPFS image handling in AnimatedNFTCard --- components/NFT/AnimatedNFTCard.tsx | 12 +- components/NFT/LazyImage.tsx | 197 ++++++++--------------------- lib/api/nftService.ts | 153 ++++++++++++++++++++++ lib/utils/ipfsUtils.ts | 112 ++++++++++++++++ 4 files changed, 328 insertions(+), 146 deletions(-) create mode 100644 lib/utils/ipfsUtils.ts diff --git a/components/NFT/AnimatedNFTCard.tsx b/components/NFT/AnimatedNFTCard.tsx index e76f182..5f6fe9a 100644 --- a/components/NFT/AnimatedNFTCard.tsx +++ b/components/NFT/AnimatedNFTCard.tsx @@ -4,6 +4,7 @@ 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'; interface NFT { id: string; @@ -30,6 +31,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); @@ -235,11 +239,11 @@ export default function AnimatedNFTCard({ nft, onClick, index = 0, isVirtualized {/* NFT Image with progressive loading */}
setImageLoaded(true)} onError={() => {/* Error handled inside LazyImage */}} 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/lib/api/nftService.ts b/lib/api/nftService.ts index 5a13450..3b1430b 100644 --- a/lib/api/nftService.ts +++ b/lib/api/nftService.ts @@ -15,6 +15,7 @@ import { fetchCollectionNFTs as alchemyFetchCollectionNFTs } from './alchemyNFTApi'; import { getChainProvider, getExplorerUrl, chainConfigs } from './chainProviders'; +import { fetchIpfsJson, ipfsUriToGatewayUrl } from '@/lib/utils/ipfsUtils'; // Environment variables for API keys const ALCHEMY_API_KEY = process.env.NEXT_PUBLIC_ALCHEMY_API_KEY || 'demo'; @@ -38,6 +39,14 @@ const collectionNFTsCache = new Map = {}; +const collectionCache: Record = {}; + +// Add pagination cache for optimizing collection browsing +type PaginationCacheKey = `${string}:${string}:${number}:${number}:${string}:${string}:${string}`; +const paginationCache: Record = {}; + /** * Chain ID to network mapping for API endpoints */ @@ -2712,3 +2721,147 @@ export function applyFilters( return filtered; } +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + +// fixxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +/** + * Fetch metadata for a token URI with caching and IPFS gateway fallbacks + * @param tokenUri URI of the token metadata + * @returns Processed metadata + */ +export async function fetchTokenMetadata(tokenUri: string) { + // Check cache first + if (metadataCache[tokenUri]) { + return metadataCache[tokenUri]; + } + + try { + let metadata; + + if (tokenUri.includes('ipfs') || tokenUri.startsWith('Qm')) { + // Use IPFS utility for IPFS URIs to handle CORS issues + metadata = await fetchIpfsJson(tokenUri); + } else { + // Regular HTTP fetch for non-IPFS URIs + const response = await fetch(tokenUri); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + metadata = await response.json(); + } + + // Process image URL if it's an IPFS URL + if (metadata.image && (metadata.image.includes('ipfs') || metadata.image.startsWith('Qm'))) { + metadata.image = ipfsUriToGatewayUrl(metadata.image); + } + + // Cache the result + metadataCache[tokenUri] = metadata; + return metadata; + } catch (error) { + console.error(`Error fetching metadata from ${tokenUri}:`, error); + + // Return basic metadata on error + return { + name: 'Unknown NFT', + description: 'Metadata could not be loaded', + image: '/images/placeholder-nft.png' // Make sure you have a placeholder image + }; + } +} + +/** + * Fetch paginated NFTs with caching (alternative implementation) + */ +export async function fetchPaginatedNFTsWithCache( + contractAddress: string, + chainId: string, + page: number = 1, + pageSize: number = 20, + sortBy: string = 'tokenId', + sortDirection: 'asc' | 'desc' = 'asc', + searchQuery: string = '', + attributes: Record = {} +) { + // Generate cache key based on params + const cacheKey: PaginationCacheKey = `${contractAddress}:${chainId}:${page}:${pageSize}:${sortBy}:${sortDirection}:${searchQuery}`; + + // Check cache + if (paginationCache[cacheKey]) { + console.log("Using cached NFT data"); + return paginationCache[cacheKey]; + } + + // Implement the actual fetch logic here + // This is a placeholder - replace with your actual implementation + const nftData = { + nfts: [], + totalPages: 1, + totalCount: 0 + }; + + // Cache the result + paginationCache[cacheKey] = nftData; + return nftData; +} + +/** + * Clear collection info cache for a specific collection and chain + */ +export function clearCollectionInfoCache(contractAddress: string, chainId: string) { + const cacheKey = `${contractAddress}:${chainId}`; + if (collectionCache[cacheKey]) { + delete collectionCache[cacheKey]; + } +} + +/** + * Clear pagination key-value cache for a specific collection and chain + */ +export function clearPaginationKeyCache(contractAddress: string, chainId: string) { + // Clear all cache entries that match the contract and chain + Object.keys(paginationCache).forEach(key => { + if (key.startsWith(`${contractAddress}:${chainId}:`)) { + delete paginationCache[key as PaginationCacheKey]; + } + }); +} + +// Other NFT service functions diff --git a/lib/utils/ipfsUtils.ts b/lib/utils/ipfsUtils.ts new file mode 100644 index 0000000..9ea7fce --- /dev/null +++ b/lib/utils/ipfsUtils.ts @@ -0,0 +1,112 @@ +/** + * IPFS URI utilities for handling CORS issues and gateway fallbacks + */ + +// A list of public IPFS gateways to try in order +const IPFS_GATEWAYS = [ + 'https://ipfs.io/ipfs/', + 'https://cloudflare-ipfs.com/ipfs/', + 'https://gateway.pinata.cloud/ipfs/', + 'https://ipfs.fleek.co/ipfs/', + 'https://dweb.link/ipfs/', + 'https://ipfs.infura.io/ipfs/' +]; + +/** + * Converts an IPFS URI (ipfs:// or ipfs/hash) to an HTTP URL using a gateway + * @param uri The IPFS URI to convert + * @param preferredGateway Optional preferred gateway URL + * @returns HTTP URL + */ +export function ipfsUriToGatewayUrl(uri: string, preferredGateway?: string): string { + if (!uri) return ''; + + // If already an HTTP URL, return as is + if (uri.startsWith('http://') || uri.startsWith('https://')) { + return uri; + } + + let ipfsHash = uri; + + // Extract the hash from various IPFS URI formats + if (uri.startsWith('ipfs://')) { + ipfsHash = uri.replace('ipfs://', ''); + } else if (uri.startsWith('ipfs/')) { + ipfsHash = uri.replace('ipfs/', ''); + } + + // Remove any leading slashes + ipfsHash = ipfsHash.replace(/^\/+/, ''); + + // Use the preferred gateway if provided + if (preferredGateway) { + const gateway = preferredGateway.endsWith('/') ? preferredGateway : `${preferredGateway}/`; + return `${gateway}${ipfsHash}`; + } + + // Default to first gateway in the list + return `${IPFS_GATEWAYS[0]}${ipfsHash}`; +} + +/** + * Fetches content from an IPFS URI with fallbacks to other gateways if the first fails + * @param uri IPFS URI to fetch + * @returns Promise with the fetch response + */ +export async function fetchFromIpfs(uri: string): Promise { + // Extract hash regardless of format + let ipfsHash = ''; + if (uri.startsWith('ipfs://')) { + ipfsHash = uri.replace('ipfs://', ''); + } else if (uri.startsWith('ipfs/')) { + ipfsHash = uri.replace('ipfs/', ''); + } else if (uri.includes('ipfs/')) { + // Handle URLs like https://gateway.com/ipfs/hash + ipfsHash = uri.split('ipfs/')[1]; + } else { + // Assume it's a direct hash + ipfsHash = uri; + } + + // Remove any leading slashes + ipfsHash = ipfsHash.replace(/^\/+/, ''); + + // Try each gateway in order until one succeeds + let lastError = null; + + for (const gateway of IPFS_GATEWAYS) { + try { + const url = `${gateway}${ipfsHash}`; + console.log(`Trying IPFS gateway: ${url}`); + + const response = await fetch(url, { + headers: { + 'Accept': 'application/json, text/plain, */*', + }, + // Set cache policy to help with repeated requests + cache: 'force-cache', + }); + + if (response.ok) { + console.log(`Successfully fetched from ${gateway}`); + return response; + } + } catch (error) { + console.warn(`Failed to fetch from ${gateway}:`, error); + lastError = error; + } + } + + // All gateways failed + throw new Error(`Failed to fetch from all IPFS gateways: ${lastError}`); +} + +/** + * Fetches JSON metadata from an IPFS URI with fallbacks + * @param uri IPFS URI to fetch + * @returns Parsed JSON data + */ +export async function fetchIpfsJson(uri: string): Promise { + const response = await fetchFromIpfs(uri); + return await response.json(); +} From a18a0da53e6887d73b1af1f8c3b2321ab61fc2cd Mon Sep 17 00:00:00 2001 From: HungPhan-0612 <163500971+HungPhan-0612@users.noreply.github.com> Date: Wed, 26 Mar 2025 14:20:27 +0700 Subject: [PATCH 06/10] Add Alchemy API routes for fetching block and transaction details --- app/api/alchemy-block-txns/route.ts | 114 +++++ app/api/alchemy-block/route.ts | 84 ++++ app/api/alchemy-token/route.ts | 170 ++++++++ app/api/alchemy-txnhash/route.ts | 95 +++++ app/block-txns/page.tsx | 271 ++++++++++++ app/block/page.tsx | 253 ++++++++++++ app/token/page.tsx | 388 ++++++++++++++++++ app/txn-hash/page.tsx | 323 +++++++++++++++ .../search-offchain/SearchBarOffChain.tsx | 206 ++++++++-- components/search/SearchBar.tsx | 206 ++++++++-- package-lock.json | 355 +++++++++++----- package.json | 1 + 12 files changed, 2323 insertions(+), 143 deletions(-) create mode 100644 app/api/alchemy-block-txns/route.ts create mode 100644 app/api/alchemy-block/route.ts create mode 100644 app/api/alchemy-token/route.ts create mode 100644 app/api/alchemy-txnhash/route.ts create mode 100644 app/block-txns/page.tsx create mode 100644 app/block/page.tsx create mode 100644 app/token/page.tsx create mode 100644 app/txn-hash/page.tsx 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/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 +
+ ) + }