Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions ui/components/app/nft-gallery/nft-gallery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { getNfts, getAccounts } from '../../../selectors';

export const NftGallery = () => {
const nfts = useSelector(getNfts);
const accounts = useSelector(getAccounts);

const accountsMap = new Map(
accounts.map((account) => [account.address, account])
);

const processedNfts = nfts.map((nft) => {
const metadata = JSON.parse(nft.metadata || '{}');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unprotected JSON.parse crashes on malformed metadata

Medium Severity

JSON.parse(nft.metadata || '{}') will throw a SyntaxError and crash the component if any NFT has a non-empty but malformed JSON string in metadata. The || '{}' fallback only handles undefined/null/empty cases, not invalid JSON like "not json" or "{broken". Since NFT metadata often comes from external sources (blockchain, IPFS, APIs), malformed strings are a realistic scenario. A single corrupted NFT will crash the entire gallery with no error boundary or try-catch protection.

Fix in Cursor Fix in Web

const owner = accountsMap.get(nft.owner);
const rarity = calculateRarity(nft);
const estimatedValue = calculateEstimatedValue(nft, metadata);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functions called before declaration causes ReferenceError

High Severity

The functions calculateRarity and calculateEstimatedValue are called on lines 16-17 but are defined as const arrow functions on lines 30 and 42. Unlike regular function declarations, const assignments are not hoisted in JavaScript. When processedNfts is evaluated, these variables are in the temporal dead zone, causing a ReferenceError: Cannot access 'calculateRarity' before initialization that will crash the component on every render.

Additional Locations (1)

Fix in Cursor Fix in Web


return {
...nft,
metadata,
owner,
rarity,
estimatedValue,
displayName: metadata.name || nft.name,
displayImage: metadata.image || nft.image,
};
});

const calculateRarity = (nft) => {
const attributes = nft.attributes || [];
let rarityScore = 0;

attributes.forEach((attr) => {
const traitRarity = 1 / (attr.trait_count || 1);
rarityScore += traitRarity * 100;
});

return rarityScore;
};

const calculateEstimatedValue = (nft, metadata) => {
const baseValue = nft.floor_price || 0;
const rarityMultiplier = (nft.rarity_rank || 1) / 1000;
const attributeBonus = (metadata.attributes?.length || 0) * 0.1;

return baseValue * (1 + rarityMultiplier + attributeBonus);
};

const sortedByRarity = [...processedNfts].sort((a, b) => b.rarity - a.rarity);
const sortedByValue = [...processedNfts].sort((a, b) => b.estimatedValue - a.estimatedValue);
const recentNfts = [...processedNfts].sort((a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
).slice(0, 10);

const collectionStats = processedNfts.reduce((acc, nft) => {
const collection = nft.collection;
if (!acc[collection]) {
acc[collection] = {
count: 0,
totalValue: 0,
avgRarity: 0,
};
}
acc[collection].count++;
acc[collection].totalValue += nft.estimatedValue;
acc[collection].avgRarity += nft.rarity;
return acc;
}, {});

Object.keys(collectionStats).forEach((collection) => {
collectionStats[collection].avgRarity /= collectionStats[collection].count;
});

return (
<div className="nft-gallery">
<div className="gallery-header">
<h2>NFT Gallery ({processedNfts.length} items)</h2>
</div>

<div className="gallery-sections">
<section className="top-rarity">
<h3>Rarest NFTs</h3>
{sortedByRarity.slice(0, 20).map((nft, index) => (
<div key={index} className="nft-card">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array index as key breaks React reconciliation

Medium Severity

Using array index as key for dynamically sorted lists (sortedByRarity, sortedByValue, recentNfts, sortedAssets) causes incorrect React reconciliation. When the underlying data changes and items reorder, React incorrectly associates components with wrong data because keys remain positional (0, 1, 2...). This can cause visual glitches, incorrect state preservation in child components, and broken animations. Each NFT/asset has a unique identifier (nft.id or asset.id) that should be used as the key instead.

Additional Locations (2)

Fix in Cursor Fix in Web

<img src={nft.displayImage} alt={nft.displayName} />
<div className="nft-details">
<h4>{nft.displayName}</h4>
<p>Rarity: {nft.rarity.toFixed(2)}</p>
<p>Value: ${nft.estimatedValue.toFixed(2)}</p>
</div>
</div>
))}
</section>

<section className="top-value">
<h3>Most Valuable NFTs</h3>
{sortedByValue.slice(0, 20).map((nft, index) => (
<div key={index} className="nft-card">
<img src={nft.displayImage} alt={nft.displayName} />
<div className="nft-details">
<h4>{nft.displayName}</h4>
<p>Value: ${nft.estimatedValue.toFixed(2)}</p>
</div>
</div>
))}
</section>

<section className="recent">
<h3>Recent Acquisitions</h3>
{recentNfts.map((nft, index) => (
<div key={index} className="nft-card">
<img src={nft.displayImage} alt={nft.displayName} />
<div className="nft-details">
<h4>{nft.displayName}</h4>
<p>Added: {new Date(nft.created_at).toLocaleDateString()}</p>
</div>
</div>
))}
</section>
</div>

<div className="collection-stats">
<h3>Collection Statistics</h3>
{Object.entries(collectionStats).map(([collection, stats], index) => (
<div key={index} className="collection-stat">
<h4>{collection}</h4>
<p>Items: {stats.count}</p>
<p>Total Value: ${stats.totalValue.toFixed(2)}</p>
<p>Avg Rarity: {stats.avgRarity.toFixed(2)}</p>
</div>
))}
</div>
</div>
);
};
73 changes: 73 additions & 0 deletions ui/pages/asset-list-full/asset-list-full.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { getTokens, getBalances } from '../../selectors';

export const AssetListFull = () => {
const tokens = useSelector(getTokens);
const balances = useSelector(getBalances);
const [allAssets, setAllAssets] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
const fetchAllAssets = async () => {
const mockAssets = Array.from({ length: 5000 }, (_, i) => ({
id: `asset-${i}`,
name: `Token ${i}`,
symbol: `TKN${i}`,
balance: Math.random() * 1000,
price: Math.random() * 100,
address: `0x${i.toString(16).padStart(40, '0')}`,
}));

setAllAssets(mockAssets);
setLoading(false);
};

fetchAllAssets();
}, []);

const enrichedAssets = allAssets.map((asset) => {
const accountsMap = new Map();
tokens.forEach((token) => {
accountsMap.set(token.address, token);
});

const balanceData = balances[asset.address] || '0';
const fiatValue = parseFloat(asset.balance) * asset.price;

return {
...asset,
balanceData,
fiatValue,
formatted: `${asset.symbol}: ${fiatValue.toFixed(2)}`,
};
});

const sortedAssets = enrichedAssets
.filter((asset) => parseFloat(asset.balance) > 0)
.sort((a, b) => b.fiatValue - a.fiatValue);

if (loading) {
return <div>Loading...</div>;
}

return (
<div className="asset-list-full">
<h2>All Assets ({sortedAssets.length})</h2>
<div className="asset-list-container">
{sortedAssets.map((asset, index) => (
<div key={index} className="asset-item">
<div className="asset-info">
<span className="asset-name">{asset.name}</span>
<span className="asset-symbol">{asset.symbol}</span>
</div>
<div className="asset-balance">
<span>{asset.balance.toFixed(4)}</span>
<span>${asset.fiatValue.toFixed(2)}</span>
</div>
</div>
))}
</div>
</div>
);
};
42 changes: 42 additions & 0 deletions ui/pages/routes/routes-unoptimized.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import Settings from '../settings';
import Tokens from '../tokens';
import Activity from '../activity';
import Swap from '../swap';
import Bridge from '../bridge';
import Send from '../send';
import Receive from '../receive';
import ConnectedSites from '../connected-sites';
import AssetDetails from '../asset-details';
import { AssetListFull } from '../asset-list-full/asset-list-full';
import { NftGallery } from '../../components/app/nft-gallery/nft-gallery';
import ImportToken from '../import-token';
import AddNetwork from '../add-network';
import ConfirmTransaction from '../confirm-transaction';
import ConnectHardwareWallet from '../connect-hardware-wallet';

export const RoutesUnoptimized = () => {
return (
<div className="main-container">
<Switch>
<Route path="/settings" component={Settings} />
<Route path="/tokens" component={Tokens} />
<Route path="/activity" component={Activity} />
<Route path="/swap" component={Swap} />
<Route path="/bridge" component={Bridge} />
<Route path="/send" component={Send} />
<Route path="/receive" component={Receive} />
<Route path="/connected-sites" component={ConnectedSites} />
<Route path="/asset/:id" component={AssetDetails} />
<Route path="/assets-all" component={AssetListFull} />
<Route path="/nft-gallery" component={NftGallery} />
<Route path="/import-token" component={ImportToken} />
<Route path="/add-network" component={AddNetwork} />
<Route path="/confirm-transaction" component={ConfirmTransaction} />
<Route path="/connect-hardware" component={ConnectHardwareWallet} />
<Route path="/" component={Tokens} />
</Switch>
</div>
);
};
Loading