Skip to content

Commit 4dde1bf

Browse files
jlobue10claude
andcommitted
Merge branch 'nft-3-preview-hardening' into nft-4-ipfs-gateway
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017NXWAjaHb9SFafLeguTHd8
2 parents fdac604 + e2957c7 commit 4dde1bf

4 files changed

Lines changed: 55 additions & 3 deletions

File tree

packages/gui/src/components/nfts/NFTPreview.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import VideoSmallIcon from '../../assets/img/video-small.svg';
2323
import VideoPngIcon from '../../assets/img/video.png';
2424
import VideoPngDarkIcon from '../../assets/img/video_dark.png';
2525
import FileType from '../../constants/FileType';
26+
import { isSettledHashMismatch } from '../../hooks/selectNFTPreviewState';
2627
import useCache from '../../hooks/useCache';
2728
import useFileType from '../../hooks/useFileType';
2829
import useHideObjectionableContent from '../../hooks/useHideObjectionableContent';
@@ -201,7 +202,11 @@ export default function NFTPreview(props: NFTPreviewProps) {
201202

202203
const previewExtension = useMemo(() => getFileExtension(preview?.uri), [preview]);
203204

204-
const previewUri = preview?.uri;
205+
// The cached bytes of a settled mismatch must never reach the iframe, even
206+
// though the state still carries the uri so the hash badge can report it.
207+
const isHashMismatch = isSettledHashMismatch(preview);
208+
209+
const previewUri = isHashMismatch ? undefined : preview?.uri;
205210

206211
const preparePreview = useCallback(
207212
async (signal: AbortSignal) => {
@@ -520,6 +525,12 @@ export default function NFTPreview(props: NFTPreviewProps) {
520525
<Trans>No file available</Trans>
521526
</IconMessage>
522527
</Background>
528+
) : isHashMismatch ? (
529+
<Background>
530+
<IconMessage icon={<NotInterested fontSize="large" />}>
531+
<Trans>File does not match the expected hash</Trans>
532+
</IconMessage>
533+
</Background>
523534
) : usesIframe && prepareError ? (
524535
<Background>
525536
<IconMessage icon={<NotInterested fontSize="large" />}>

packages/gui/src/hooks/selectNFTPreviewState.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import selectNFTPreviewState, { type NFTPreviewState } from './selectNFTPreviewState';
1+
import selectNFTPreviewState, { isSettledHashMismatch, type NFTPreviewState } from './selectNFTPreviewState';
22

33
const fetchFailure: NFTPreviewState = {
44
isVerified: false,
@@ -102,3 +102,18 @@ describe('selectNFTPreviewState', () => {
102102
).toBe(verifiedVideo);
103103
});
104104
});
105+
106+
describe('isSettledHashMismatch', () => {
107+
it('flags a settled mismatch so its bytes are never rendered', () => {
108+
expect(isSettledHashMismatch(hashMismatch)).toBe(true);
109+
});
110+
111+
it('does not flag verified, optimistic, failed-fetch, or missing states', () => {
112+
expect(isSettledHashMismatch(undefined)).toBe(false);
113+
expect(isSettledHashMismatch({ isVerified: true, uri: 'https://example.com/ok.png' })).toBe(false);
114+
expect(
115+
isSettledHashMismatch({ isVerified: false, isVerifying: true, uri: 'https://example.com/pending.png' }),
116+
).toBe(false);
117+
expect(isSettledHashMismatch(fetchFailure)).toBe(false);
118+
});
119+
});

packages/gui/src/hooks/selectNFTPreviewState.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ export type SelectNFTPreviewStateOptions = {
2323
previewImageCandidate?: PreviewCandidate;
2424
};
2525

26+
// A settled checksum mismatch: verification ran to completion and the bytes
27+
// do not match the on-chain hash. The state still carries the uri so status
28+
// badges can report which file failed, but consumers must never render that
29+
// uri as media content — the cached bytes are by definition not the content
30+
// the hash commits to.
31+
export function isSettledHashMismatch(state: NFTPreviewState | undefined): boolean {
32+
return !!state && !state.isVerified && !state.isVerifying && state.failedFetch === false;
33+
}
34+
2635
function asCandidate(candidate: PreviewCandidate | undefined): NFTPreviewState | undefined {
2736
const uri = candidate?.uris?.[0];
2837

packages/gui/src/hooks/useNFTVerifyHash.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,23 @@ export default function useNFTVerifyHash(nftId?: string, options: UseNFTVerifyHa
3535
const dataGeneration = useRef(0);
3636
const previewGeneration = useRef(0);
3737

38-
const isVerifying = isVerifyingData || isVerifyingPreview;
38+
// The inputs the preview effect last picked up. `isVerifyingPreview` is set
39+
// inside that effect, which runs only after the render has painted, so on
40+
// the frame where the metadata first arrives it still reads false — and an
41+
// already-verified data file would win the preview slot for that one frame
42+
// before the swap. Comparing the current inputs against this ref makes that
43+
// frame count as verifying synchronously.
44+
const previewInputs = useRef<{ nft?: NFTInfo; metadata?: Metadata }>({});
45+
46+
const settledMetadata = isLoadingMetadata ? undefined : metadata;
47+
const isPreviewPassPending =
48+
preview &&
49+
!!nft &&
50+
!isLoadingNFT &&
51+
!!settledMetadata &&
52+
(previewInputs.current.nft !== nft || previewInputs.current.metadata !== settledMetadata);
53+
54+
const isVerifying = isVerifyingData || isVerifyingPreview || isPreviewPassPending;
3955

4056
// A pending metadata download only blocks the result while there is no
4157
// data verification outcome yet: `isVerified` is derived from the data
@@ -182,6 +198,7 @@ export default function useNFTVerifyHash(nftId?: string, options: UseNFTVerifyHa
182198
// verifies the data file right away, and this effect picks up the preview
183199
// URIs once the metadata fetch settles, instead of blocking on it.
184200
const nftMetadata = isLoadingMetadata ? undefined : metadata;
201+
previewInputs.current = { nft: !isLoadingNFT ? nft : undefined, metadata: nftMetadata };
185202
if (!preview || !nft || isLoadingNFT || !nftMetadata) {
186203
setIsVerifyingPreview(false);
187204
} else {

0 commit comments

Comments
 (0)