Skip to content

Commit

Permalink
NFT Details - make it work based on only url params (#1750)
Browse files Browse the repository at this point in the history
  • Loading branch information
greg-schrammel authored Dec 10, 2024
1 parent 705fb8e commit d380406
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 62 deletions.
46 changes: 45 additions & 1 deletion src/core/network/nfts.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { RainbowError, logger } from '~/logger';

import { queryClient } from '../react-query';
import { ChainName } from '../types/chains';
import { ChainId, ChainName, chainNameToIdMapping } from '../types/chains';
import {
PolygonAllowListDictionary,
SimpleHashCollectionDetails,
SimpleHashNFT,
UniqueAsset,
} from '../types/nfts';
import {
simpleHashNFTToUniqueAsset,
simpleHashSupportedChainNames,
simpleHashSupportedTestnetChainNames,
validateSimpleHashNFT,
} from '../utils/nfts';

import { RainbowFetchClient } from './internal/rainbowFetch';
import { nftAllowListClient } from './nftAllowList';
Expand Down Expand Up @@ -185,3 +191,41 @@ export const reportNftAsSpam = async (nft: UniqueAsset) => {
});
}
};

const simplehashChainNames = [
...simpleHashSupportedChainNames,
...simpleHashSupportedTestnetChainNames,
];

/**
* @throws when chain is not supported
* @throws when fetching simplehash fails for some reason
*/
export const fetchNft = async ({
contractAddress,
tokenId,
chainId,
}: {
contractAddress: string;
chainId: ChainId;
tokenId: string;
}) => {
const chain = simplehashChainNames.find(
(chainName) => chainNameToIdMapping[chainName] === chainId,
);
if (!chain) throw new Error('Chain not supported');

try {
const response = await nftApi.get<SimpleHashNFT>(
`/nfts/${chain}/${contractAddress}/${tokenId}`,
);
const validatedNft = validateSimpleHashNFT(response.data);
if (!validatedNft) throw new Error('Invalid NFT');
return simpleHashNFTToUniqueAsset(validatedNft);
} catch (e) {
logger.error(new RainbowError('Fetch NFT: '), {
message: (e as Error)?.message,
});
throw new Error('Failed to fetch Nft');
}
};
34 changes: 34 additions & 0 deletions src/core/resources/nfts/useNft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useQuery } from '@tanstack/react-query';
import { Address } from 'viem';

import { fetchNft } from '~/core/network/nfts';
import { createQueryKey } from '~/core/react-query';
import { ChainId } from '~/core/types/chains';
import { UniqueAsset } from '~/core/types/nfts';

export function useNft(
{
contractAddress,
chainId,
tokenId,
}: {
contractAddress: Address;
chainId: ChainId;
tokenId: string;
},
{ initialData }: { initialData: UniqueAsset },
) {
return useQuery({
queryKey: createQueryKey(
'nft',
{ contractAddress, chainId, tokenId },
{ persisterVersion: 1 },
),
queryFn: ({ queryKey }) => fetchNft(queryKey[0]),
initialData,
initialDataUpdatedAt: initialData !== undefined ? Date.now() : 0,
enabled: !!contractAddress && !!chainId && !!tokenId,
retry: 3,
staleTime: 24 * 60 * 60 * 1000, // 1 day
});
}
77 changes: 40 additions & 37 deletions src/core/utils/nfts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,47 +190,50 @@ export function getNetworkFromSimpleHashChain(
}
}

export function validateSimpleHashNFT(
nft: SimpleHashNFT,
allowList?: PolygonAllowListDictionary,
): ValidatedSimpleHashNFT | undefined {
const lowercasedContractAddress = nft.contract_address?.toLowerCase();
const network = getNetworkFromSimpleHashChain(nft.chain);

const isMissingRequiredFields =
!nft.name ||
!nft.collection?.name ||
!nft.contract_address ||
!nft.token_id ||
!network;
const isPolygonAndNotAllowed =
allowList &&
nft.chain === SimpleHashChain.Polygon &&
!allowList[lowercasedContractAddress];
const isGnosisAndNotPOAP =
nft.chain === SimpleHashChain.Gnosis &&
lowercasedContractAddress !== POAP_NFT_ADDRESS;

if (isMissingRequiredFields || isPolygonAndNotAllowed || isGnosisAndNotPOAP) {
return undefined;
}

return {
...nft,
name: nft.name || '',
contract_address: nft.contract_address,
chain: getNetworkFromSimpleHashChain(nft.chain),
collection: { ...nft.collection, name: nft.collection.name || '' },
token_id: nft.token_id || '',
};
}

export function filterSimpleHashNFTs(
nfts: SimpleHashNFT[],
allowList: PolygonAllowListDictionary,
): ValidatedSimpleHashNFT[] {
return nfts
.filter((nft) => {
const lowercasedContractAddress = nft.contract_address?.toLowerCase();
const network = getNetworkFromSimpleHashChain(nft.chain);

const isMissingRequiredFields =
!nft.name ||
!nft.collection?.name ||
!nft.contract_address ||
!nft.token_id ||
!network;
const isPolygonAndNotAllowed =
allowList &&
nft.chain === SimpleHashChain.Polygon &&
!allowList[lowercasedContractAddress];
const isGnosisAndNotPOAP =
nft.chain === SimpleHashChain.Gnosis &&
lowercasedContractAddress !== POAP_NFT_ADDRESS;

if (
isMissingRequiredFields ||
isPolygonAndNotAllowed ||
isGnosisAndNotPOAP
) {
return false;
}

return true;
})
.map((nft) => ({
...nft,
name: nft.name || '',
contract_address: nft.contract_address,
chain: getNetworkFromSimpleHashChain(nft.chain),
collection: { ...nft.collection, name: nft.collection.name || '' },
token_id: nft.token_id || '',
}));
return nfts.reduce((validatedNfts, nft) => {
const validatedNft = validateSimpleHashNFT(nft, allowList);
if (validatedNft) validatedNfts.push(validatedNft);
return validatedNfts;
}, [] as ValidatedSimpleHashNFT[]);
}

export function extractPoapDropId(externalUrl: string) {
Expand Down
6 changes: 3 additions & 3 deletions src/entries/popup/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { Home } from './pages/home';
import { ActivityDetails } from './pages/home/Activity/ActivityDetails';
import { Approvals } from './pages/home/Approvals/Approvals';
import { ConnectedApps } from './pages/home/ConnectedApps';
import NFTDetails from './pages/home/NFTs/NFTDetails';
import { NftDetailsRoute } from './pages/home/NFTs/NFTDetails';
import { ClaimSheet } from './pages/home/Points/ClaimSheet';
import { PointsOnboardingSheet } from './pages/home/Points/PointsOnboardingSheet';
import { PointsReferralSheet } from './pages/home/Points/PointsReferralSheet';
Expand Down Expand Up @@ -189,10 +189,10 @@ const ROUTE_DATA = [
],
},
{
path: ROUTES.NFT_DETAILS(':collectionId', ':nftId'),
path: ROUTES.NFT_DETAILS(':collectionUniqueId', ':tokenId'),
element: (
<AnimatedRoute direction="right">
<NFTDetails />
<NftDetailsRoute />
</AnimatedRoute>
),
},
Expand Down
17 changes: 13 additions & 4 deletions src/entries/popup/components/CommandK/useSearchableNFTs.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useCallback, useEffect, useMemo } from 'react';
import { Address } from 'viem';

import { i18n } from '~/core/languages';
import { selectNfts } from '~/core/resources/_selectors/nfts';
import { useGalleryNfts } from '~/core/resources/nfts/galleryNfts';
import { useCurrentAddressStore } from '~/core/state';
import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode';
import { useNftsStore } from '~/core/state/nfts';
import { ChainName, chainNameToIdMapping } from '~/core/types/chains';
import { UniqueAsset } from '~/core/types/nfts';

import { useRainbowNavigate } from '../../hooks/useRainbowNavigate';
Expand Down Expand Up @@ -84,10 +86,17 @@ export const useSearchableNFTs = () => {

const searchableNFTs = useMemo(() => {
return nfts.map<NFTSearchItem>((nft) => ({
action: () =>
navigate(
ROUTES.NFT_DETAILS(nft.collection.collection_id || '', nft.id),
),
action: () => {
const [chainName, contractAddress, tokenId] = nft.fullUniqueId.split(
'_',
) as [ChainName, Address, string];
const chainId = chainNameToIdMapping[chainName];
if (!chainId) return;
return navigate(
ROUTES.NFT_DETAILS(`${contractAddress}_${chainId}`, tokenId),
{ state: { nft } },
);
},
actionLabel: actionLabels.open,
actionPage: PAGES.NFT_TOKEN_DETAIL,
id: nft.uniqueId,
Expand Down
51 changes: 44 additions & 7 deletions src/entries/popup/pages/home/NFTs/NFTDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import { DropdownMenuRadioGroup } from '@radix-ui/react-dropdown-menu';
import clsx from 'clsx';
import { format, formatDistanceStrict } from 'date-fns';
import { ReactNode, useCallback, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { Navigate, useLocation, useParams } from 'react-router-dom';
import { Address } from 'viem';
import { useEnsName } from 'wagmi';

import { i18n } from '~/core/languages';
import { chainsLabel } from '~/core/references/chains';
import { useEnsRegistration } from '~/core/resources/ens/ensRegistration';
import { useNft } from '~/core/resources/nfts/useNft';
import { useSelectedNftStore } from '~/core/state/selectedNft';
import { AddressOrEth } from '~/core/types/assets';
import { AddressOrEth, UniqueId } from '~/core/types/assets';
import { ChainId, ChainName, chainNameToIdMapping } from '~/core/types/chains';
import { UniqueAsset } from '~/core/types/nfts';
import { truncateAddress } from '~/core/utils/address';
Expand Down Expand Up @@ -81,13 +82,27 @@ import NFTContextMenu from './NFTContextMenu';
import NFTDropdownMenu from './NFTDropdownMenu';
import { getOpenseaUrl, getRaribleUrl } from './utils';

export default function NFTDetails() {
const { state } = useLocation();
const nft = state?.nft;
function NFTDetails({
chainId,
contractAddress,
tokenId,
initialData,
}: {
chainId: ChainId;
contractAddress: Address;
tokenId: string;
initialData: UniqueAsset;
}) {
const { data: nft } = useNft(
{ contractAddress, chainId, tokenId },
{ initialData },
);

const isPOAP = nft?.familyName === 'POAP';
const navigate = useRainbowNavigate();
const { isWatchingWallet } = useWallets();
const { setSelectedNft } = useSelectedNftStore();

const {
ensAddress,
ensBio,
Expand Down Expand Up @@ -130,8 +145,7 @@ export default function NFTDetails() {
labelColor: 'label',
},
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [showExplainerSheet, hideExplainerSheet]);

useNftShortcuts(nft);

Expand Down Expand Up @@ -1279,3 +1293,26 @@ export const NFTInfoRow = ({
</Box>
</Box>
);

const parseUniqueId = (uniqueId: string | undefined) =>
(uniqueId?.split('_') || []) as [address?: Address, chainId?: ChainId];
export function NftDetailsRoute() {
const { state } = useLocation();
const { collectionUniqueId, tokenId } = useParams<{
collectionUniqueId: UniqueId;
tokenId: string;
}>();
const [contractAddress, chainId] = parseUniqueId(collectionUniqueId);

if (!contractAddress || !chainId || !tokenId) {
return <Navigate to={ROUTES.HOME} state={{ tab: 'nfts' }} replace />;
}
return (
<NFTDetails
chainId={+chainId}
contractAddress={contractAddress}
tokenId={tokenId}
initialData={state.nft}
/>
);
}
18 changes: 10 additions & 8 deletions src/entries/popup/pages/home/NFTs/NFTs.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React, { useRef } from 'react';
import { Address } from 'viem';

import { useCurrentAddressStore } from '~/core/state';
import { useTestnetModeStore } from '~/core/state/currentSettings/testnetMode';
import { useNftsStore } from '~/core/state/nfts';
import { ChainName, chainNameToIdMapping } from '~/core/types/chains';
import { UniqueAsset } from '~/core/types/nfts';
import { Bleed, Box } from '~/design-system';
import { useNftShortcuts } from '~/entries/popup/hooks/useNftShortcuts';
Expand All @@ -21,14 +23,14 @@ export function NFTs() {
const { chains: userChains } = useUserChains();
const navigate = useRainbowNavigate();
const onAssetClick = (asset: UniqueAsset) => {
navigate(
ROUTES.NFT_DETAILS(asset?.collection.collection_id || '', asset?.id),
{
state: {
nft: asset,
},
},
);
const [chainName, contractAddress, tokenId] = asset.fullUniqueId.split(
'_',
) as [ChainName, Address, string];
const chainId = chainNameToIdMapping[chainName];
if (!chainId) return;
navigate(ROUTES.NFT_DETAILS(`${contractAddress}_${chainId}`, tokenId), {
state: { nft: asset },
});
};

const groupedContainerRef = useRef<HTMLDivElement>(null);
Expand Down
4 changes: 2 additions & 2 deletions src/entries/popup/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export const ROUTES = {
`/home/token-details/${uniqueId}`,
ACTIVITY_DETAILS: (chainId: ChainId | ':chainId', hash: TxHash | ':hash') =>
`/home/activity-details/${chainId}/${hash}`,
NFT_DETAILS: (collectionId: string, nftId: string) =>
`/home/nft-details/${collectionId}/${nftId}`,
NFT_DETAILS: (collectionUniqueId: UniqueId, tokenId: string) =>
`/home/nft-details/${collectionUniqueId}/${tokenId}`,
POINTS_REFERRAL: '/home/points-referral',
POINTS_ONBOARDING: '/home/points-onboarding',
POINTS_WEEKLY_OVERVIEW: '/home/points-weekly-overview',
Expand Down

0 comments on commit d380406

Please sign in to comment.