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 && (
-
- )}
-
- {/* Error fallback */}
- {error && (
+ <>
+ {loading && showLoadingIndicator && (
)}
-
+
+
+ >
);
}
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 (
+ <>
+
+
+
+
+
+
+
+

{
+ 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 */}
+
+
+
+
+
+
+
+
+
+
+ {/* 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
+
+ )
+ }