From 092e5c7d31633778fd2a959868a61b8a55d9dbb3 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Wed, 30 Jul 2025 15:04:56 -0500 Subject: [PATCH 01/19] reusable profile component --- src/components/NftGroupCard.tsx | 7 +- src/components/Profile.tsx | 754 +++++++++++++++++++++++++++++++ src/lib/marketplaces.ts | 10 + src/pages/CollectionMetaData.tsx | 25 +- src/pages/DidList.tsx | 386 +--------------- src/pages/Nft.tsx | 68 +-- 6 files changed, 788 insertions(+), 462 deletions(-) create mode 100644 src/components/Profile.tsx diff --git a/src/components/NftGroupCard.tsx b/src/components/NftGroupCard.tsx index 9b6bea7d..96a23602 100644 --- a/src/components/NftGroupCard.tsx +++ b/src/components/NftGroupCard.tsx @@ -4,6 +4,7 @@ import { NftRecord, commands, } from '@/bindings'; +import { MintGardenProfile } from '@/components/Profile'; import { NftGroupMode } from '@/hooks/useNftParams'; import useOfferStateWithDefault from '@/hooks/useOfferStateWithDefault'; import { getMintGardenProfile } from '@/lib/marketplaces'; @@ -72,11 +73,7 @@ export function NftGroupCard({ const isCollection = type === 'collection'; // Profile state for DID cards - const [didProfile, setDidProfile] = useState<{ - encoded_id: string; - name: string; - avatar_uri: string | null; - } | null>(null); + const [didProfile, setDidProfile] = useState(null); // Fetch profile data for DID cards useEffect(() => { diff --git a/src/components/Profile.tsx b/src/components/Profile.tsx new file mode 100644 index 00000000..440b0589 --- /dev/null +++ b/src/components/Profile.tsx @@ -0,0 +1,754 @@ +import { AssetIcon } from '@/components/AssetIcon'; +import ConfirmationDialog from '@/components/ConfirmationDialog'; +import { DidConfirmation } from '@/components/confirmations/DidConfirmation'; +import { FeeOnlyDialog } from '@/components/FeeOnlyDialog'; +import { TransferDialog } from '@/components/TransferDialog'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Skeleton } from '@/components/ui/skeleton'; +import { useErrors } from '@/hooks/useErrors'; +import { getMintGardenProfile } from '@/lib/marketplaces'; +import { toMojos } from '@/lib/utils'; +import { useWalletState } from '@/state'; +import { t } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; +import { writeText } from '@tauri-apps/plugin-clipboard-manager'; +import { openUrl } from '@tauri-apps/plugin-opener'; +import { + ActivityIcon, + Copy, + ExternalLinkIcon, + EyeIcon, + EyeOff, + Flame, + MoreVerticalIcon, + PenIcon, + SendIcon, +} from 'lucide-react'; +import { useEffect, useMemo, useState } from 'react'; +import { toast } from 'react-toastify'; +import { commands, DidRecord, TransactionResponse } from '../bindings'; +import { CustomError } from '../contexts/ErrorContext'; + +export interface MintGardenProfile { + encoded_id: string; + name: string; + avatar_uri: string | null; + is_unknown: boolean; +} + +export interface ProfileProps { + // For MintGarden profiles (DID string only) + did?: string; + // For local DID records (with enhanced MintGarden data) + didRecord?: DidRecord; + // Common props + variant?: 'default' | 'compact' | 'card'; + showDid?: boolean; + className?: string; + onProfileLoad?: (profile: MintGardenProfile) => void; + // Local DID specific + updateDids?: () => void; + showMintGardenProfile?: boolean; +} + +// ProfileContent component to handle the actual rendering +interface ProfileContentProps { + didRecord?: DidRecord; + mintGardenProfile: MintGardenProfile; + isMintGardenLoading: boolean; + variant: 'default' | 'compact' | 'card'; + showDid: boolean; + className: string; + showMintGardenProfile: boolean; + name: string; + setName: (name: string) => void; + response: TransactionResponse | null; + renameOpen: boolean; + setRenameOpen: (open: boolean) => void; + transferOpen: boolean; + setTransferOpen: (open: boolean) => void; + burnOpen: boolean; + setBurnOpen: (open: boolean) => void; + normalizeOpen: boolean; + setNormalizeOpen: (open: boolean) => void; + isTransferring: boolean; + isBurning: boolean; + isNormalizing: boolean; + transferAddress: string; + updateDids?: () => void; + addError: (error: CustomError) => void; + walletState: { sync: { unit: { decimals: number }; burn_address: string } }; + setResponse: (response: TransactionResponse | null) => void; + setIsTransferring: (transferring: boolean) => void; + setIsBurning: (burning: boolean) => void; + setIsNormalizing: (normalizing: boolean) => void; + setTransferAddress: (address: string) => void; +} + +function ProfileContent({ + didRecord, + mintGardenProfile, + isMintGardenLoading, + variant, + showDid, + className, + name, + setName, + response, + renameOpen, + setRenameOpen, + transferOpen, + setTransferOpen, + burnOpen, + setBurnOpen, + normalizeOpen, + setNormalizeOpen, + isTransferring, + isBurning, + isNormalizing, + transferAddress, + updateDids, + addError, + walletState, + setResponse, + setIsTransferring, + setIsBurning, + setIsNormalizing, + setTransferAddress, +}: ProfileContentProps) { + // Local DID action handlers + const rename = () => { + if (!name || !didRecord) return; + + commands + .updateDid({ + did_id: didRecord.launcher_id, + name, + visible: didRecord.visible, + }) + .then(() => updateDids?.()) + .catch((err) => addError(err as CustomError)) + .finally(() => { + setRenameOpen(false); + setName(''); + }); + }; + + const toggleVisibility = () => { + if (!didRecord) return; + + commands + .updateDid({ + did_id: didRecord.launcher_id, + name: didRecord.name, + visible: !didRecord.visible, + }) + .then(() => updateDids?.()) + .catch((err) => addError(err as CustomError)); + }; + + const onTransferSubmit = (address: string, fee: string) => { + if (!didRecord) return; + + setIsTransferring(true); + setTransferAddress(address); + commands + .transferDids({ + did_ids: [didRecord.launcher_id], + address, + fee: toMojos(fee, walletState.sync.unit.decimals), + }) + .then(setResponse) + .catch((err) => { + setIsTransferring(false); + addError(err as CustomError); + }) + .finally(() => setTransferOpen(false)); + }; + + const onBurnSubmit = (fee: string) => { + if (!didRecord) return; + + setIsBurning(true); + commands + .transferDids({ + did_ids: [didRecord.launcher_id], + address: walletState.sync.burn_address, + fee: toMojos(fee, walletState.sync.unit.decimals), + }) + .then(setResponse) + .catch((err) => { + setIsBurning(false); + addError(err as CustomError); + }) + .finally(() => setBurnOpen(false)); + }; + + const onNormalizeSubmit = (fee: string) => { + if (!didRecord) return; + + setIsNormalizing(true); + commands + .normalizeDids({ + did_ids: [didRecord.launcher_id], + fee: toMojos(fee, walletState.sync.unit.decimals), + }) + .then(setResponse) + .catch((err) => { + setIsNormalizing(false); + addError(err as CustomError); + }) + .finally(() => setNormalizeOpen(false)); + }; + + const handleMintGardenClick = () => { + if (!mintGardenProfile.is_unknown) { + openUrl(`https://mintgarden.io/${mintGardenProfile.encoded_id}`); + } + }; + + // Display logic + const displayName = !mintGardenProfile.is_unknown + ? mintGardenProfile.name + : didRecord?.name || mintGardenProfile?.name || t`Untitled Profile`; + + const displayDid = mintGardenProfile.encoded_id || ''; + + // Loading state + if (isMintGardenLoading) { + return ( +
+ +
+ + {showDid && } +
+
+ ); + } + + if (variant === 'compact') { + return ( +
+ + {displayName} + {showDid && ( + + {displayDid} + + )} + {!mintGardenProfile.is_unknown && ( + + )} +
+ ); + } + + if (variant === 'card') { + return ( + <> + + + + + {displayName} + + {didRecord ? ( + + + + + + + {!mintGardenProfile.is_unknown && ( + <> + { + e.stopPropagation(); + handleMintGardenClick(); + }} + > + + View on MintGarden + + + + )} + + { + e.stopPropagation(); + setTransferOpen(true); + }} + disabled={didRecord.created_height === null} + > + + Transfer + + + {didRecord.recovery_hash === null && ( + { + e.stopPropagation(); + setNormalizeOpen(true); + }} + disabled={didRecord.created_height === null} + > + + Normalize + + )} + + { + e.stopPropagation(); + setBurnOpen(true); + }} + disabled={didRecord.created_height === null} + > + + Burn + + + + + { + e.stopPropagation(); + writeText(didRecord.launcher_id); + toast.success(t`DID ID copied to clipboard`); + }} + > + + Copy ID + + + { + e.stopPropagation(); + setRenameOpen(true); + }} + > + + Rename + + + { + e.stopPropagation(); + toggleVisibility(); + }} + > + {didRecord.visible ? ( + + ) : ( + + )} + {didRecord.visible ? t`Hide` : t`Show`} + + + + + ) : null} + + +
{displayDid}
+
+
+ + {didRecord && ( + <> + !open && setRenameOpen(false)} + > + + + + Rename Profile + + + Enter the new display name for this profile. + + +
+
+ + setName(event.target.value)} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + rename(); + } + }} + /> +
+
+ + + + +
+
+ + + This will send the profile to the provided address. + + + + + This will permanently delete the profile by sending it to the + burn address. + + + + + + This will modify the profile's recovery info to be + compatible with the Chia reference wallet. + + + + { + setResponse(null); + setIsTransferring(false); + setIsBurning(false); + setIsNormalizing(false); + }} + onConfirm={() => updateDids?.()} + additionalData={ + isTransferring && response + ? { + title: t`Transfer DID`, + content: ( + + ), + } + : isBurning && response + ? { + title: t`Burn DID`, + content: ( + + ), + } + : isNormalizing && response + ? { + title: t`Normalize DID`, + content: ( + + ), + } + : undefined + } + /> + + )} + + ); + } + + return ( +
+ +
+
{displayName}
+ {showDid && ( +
+ {displayDid} +
+ )} +
+ {!mintGardenProfile.is_unknown && ( + + )} +
+ ); +} + +// Enhanced Unified Profile Component +export function Profile({ + did, + didRecord, + variant = 'default', + showDid = false, + className = '', + showMintGardenProfile = true, + onProfileLoad, + updateDids, +}: ProfileProps) { + const { addError } = useErrors(); + const walletState = useWalletState(); + + // Determine the DID to use for MintGarden lookup + const didToLookup = didRecord?.launcher_id || did; + + // State for MintGarden profile data + const [mintGardenProfile, setMintGardenProfile] = useState( + { + encoded_id: didToLookup || '', + name: + didRecord?.name || + `${(didToLookup || '').slice(9, 19)}...${(didToLookup || '').slice(-4)}`, + avatar_uri: null, + is_unknown: true, + }, + ); + const [isMintGardenLoading, setIsMintGardenLoading] = useState(false); + + // State for local DID actions (only when didRecord is provided) + const [name, setName] = useState(''); + const [response, setResponse] = useState(null); + const [renameOpen, setRenameOpen] = useState(false); + const [transferOpen, setTransferOpen] = useState(false); + const [burnOpen, setBurnOpen] = useState(false); + const [normalizeOpen, setNormalizeOpen] = useState(false); + const [isTransferring, setIsTransferring] = useState(false); + const [isBurning, setIsBurning] = useState(false); + const [isNormalizing, setIsNormalizing] = useState(false); + const [transferAddress, setTransferAddress] = useState(''); + + // Fetch MintGarden profile data + useEffect(() => { + if (!didToLookup) return; + + if (showMintGardenProfile) { + setIsMintGardenLoading(true); + getMintGardenProfile(didToLookup) + .then((profileData) => { + setMintGardenProfile(profileData); + onProfileLoad?.(profileData); + }) + .catch(() => { + // Create fallback profile for failed lookups + setMintGardenProfile({ + encoded_id: didToLookup, + name: + didRecord?.name || + `${didToLookup.slice(9, 19)}...${didToLookup.slice(-4)}`, + avatar_uri: null, + is_unknown: true, + }); + }) + .finally(() => { + setIsMintGardenLoading(false); + }); + } else { + setMintGardenProfile({ + encoded_id: didToLookup, + name: + didRecord?.name || + `${didToLookup.slice(9, 19)}...${didToLookup.slice(-4)}`, + avatar_uri: null, + is_unknown: true, + }); + } + }, [didToLookup, didRecord?.name, onProfileLoad, showMintGardenProfile]); + + return ( + + ); +} + +// Hook for managing MintGarden profile state +export function useMintGardenProfile(did: string | undefined) { + const defaultProfile = useMemo( + () => ({ + encoded_id: did ?? '', + name: `${did?.slice(9, 19)}...${did?.slice(-4)}`, + avatar_uri: null, + is_unknown: true, + }), + [did], + ); + + const [profile, setProfile] = useState(defaultProfile); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!did) { + setProfile(defaultProfile); + setIsLoading(false); + return; + } + + setIsLoading(true); + setError(null); + + getMintGardenProfile(did) + .then((profileData) => { + setProfile(profileData); + }) + .finally(() => { + setIsLoading(false); + }); + }, [did, defaultProfile]); + + return { profile, isLoading, error }; +} + +// Clickable profile component that opens MintGarden on click +export interface ClickableProfileProps extends ProfileProps { + onClick?: (profile: MintGardenProfile) => void; +} + +export function ClickableProfile({ onClick, ...props }: ClickableProfileProps) { + const handleProfileLoad = (profile: MintGardenProfile) => { + props.onProfileLoad?.(profile); + }; + + const handleClick = () => { + const didToUse = props.didRecord?.launcher_id || props.did; + if (onClick && didToUse) { + getMintGardenProfile(didToUse).then(onClick); + } else if (didToUse) { + openUrl(`https://mintgarden.io/${didToUse}`); + } + }; + + return ( +
+ +
+ ); +} + +export default Profile; diff --git a/src/lib/marketplaces.ts b/src/lib/marketplaces.ts index d99f3f53..7249117b 100644 --- a/src/lib/marketplaces.ts +++ b/src/lib/marketplaces.ts @@ -59,14 +59,24 @@ export async function getMintGardenProfile(did: string) { try { const response = await fetch(`https://api.mintgarden.io/profile/${did}`); const data = await response.json(); + if (data?.detail === 'Unknown profile.') { + return { + encoded_id: did, + name: `${did.slice(9, 19)}...${did.slice(-4)}`, // 9 strips off "did:chia:" + avatar_uri: null, + is_unknown: true, + }; + } // always supply a name data.name = data.name || `${did.slice(9, 19)}...${did.slice(-4)}`; + data.is_unknown = false; return data; } catch { return { encoded_id: did, name: `${did.slice(9, 19)}...${did.slice(-4)}`, // 9 strips off "did:chia:" avatar_uri: null, + is_unknown: true, }; } } diff --git a/src/pages/CollectionMetaData.tsx b/src/pages/CollectionMetaData.tsx index e9b22438..29cc943b 100644 --- a/src/pages/CollectionMetaData.tsx +++ b/src/pages/CollectionMetaData.tsx @@ -2,6 +2,7 @@ import { commands, NetworkKind, NftCollectionRecord } from '@/bindings'; import Container from '@/components/Container'; import { CopyBox } from '@/components/CopyBox'; import Header from '@/components/Header'; +import Profile, { MintGardenProfile } from '@/components/Profile'; import { Button } from '@/components/ui/button'; import { CustomError } from '@/contexts/ErrorContext'; import { useErrors } from '@/hooks/useErrors'; @@ -35,11 +36,9 @@ export default function CollectionMetaData() { useState(null); const [loading, setLoading] = useState(true); const [network, setNetwork] = useState(null); - const [minterProfile, setMinterProfile] = useState<{ - encoded_id: string; - name: string; - avatar_uri: string | null; - } | null>(null); + const [minterProfile, setMinterProfile] = useState( + null, + ); useEffect(() => { async function fetchData() { @@ -379,20 +378,8 @@ export default function CollectionMetaData() { onCopy={() => toast.success(t`Minter DID copied to clipboard`)} /> {minterProfile && ( -
- openUrl(`https://mintgarden.io/${collection.did_id}`) - } - > - {minterProfile.avatar_uri && ( - {`${minterProfile.name} - )} -
{minterProfile.name}
+
+
)}
diff --git a/src/pages/DidList.tsx b/src/pages/DidList.tsx index dd893b7c..ec39cb39 100644 --- a/src/pages/DidList.tsx +++ b/src/pages/DidList.tsx @@ -1,56 +1,16 @@ -import ConfirmationDialog from '@/components/ConfirmationDialog'; -import { DidConfirmation } from '@/components/confirmations/DidConfirmation'; import Container from '@/components/Container'; -import { FeeOnlyDialog } from '@/components/FeeOnlyDialog'; import Header from '@/components/Header'; +import { Profile } from '@/components/Profile'; import { ReceiveAddress } from '@/components/ReceiveAddress'; -import { TransferDialog } from '@/components/TransferDialog'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { useDids } from '@/hooks/useDids'; -import { useErrors } from '@/hooks/useErrors'; -import { toMojos } from '@/lib/utils'; -import { useWalletState } from '@/state'; import { t } from '@lingui/core/macro'; import { Plural, Trans } from '@lingui/react/macro'; -import { writeText } from '@tauri-apps/plugin-clipboard-manager'; -import { - ActivityIcon, - Copy, - EyeIcon, - EyeOff, - Flame, - MoreVerticalIcon, - PenIcon, - SendIcon, - UserIcon, - UserPlusIcon, - UserRoundPlus, -} from 'lucide-react'; +import { UserPlusIcon, UserRoundPlus } from 'lucide-react'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; -import { commands, DidRecord, TransactionResponse } from '../bindings'; export function DidList() { const navigate = useNavigate(); @@ -103,344 +63,16 @@ export function DidList() {
{visibleDids.map((did) => ( - + ))}
); } - -interface ProfileProps { - did: DidRecord; - updateDids: () => void; -} - -function Profile({ did, updateDids }: ProfileProps) { - const { addError } = useErrors(); - - const walletState = useWalletState(); - - const [name, setName] = useState(''); - const [response, setResponse] = useState(null); - - const [renameOpen, setRenameOpen] = useState(false); - const [transferOpen, setTransferOpen] = useState(false); - const [burnOpen, setBurnOpen] = useState(false); - const [normalizeOpen, setNormalizeOpen] = useState(false); - - // Track which action is being performed - const [isTransferring, setIsTransferring] = useState(false); - const [isBurning, setIsBurning] = useState(false); - const [isNormalizing, setIsNormalizing] = useState(false); - const [transferAddress, setTransferAddress] = useState(''); - - const rename = () => { - if (!name) return; - - commands - .updateDid({ did_id: did.launcher_id, name, visible: did.visible }) - .then(updateDids) - .catch(addError) - .finally(() => { - setRenameOpen(false); - setName(''); - }); - }; - - const toggleVisibility = () => { - commands - .updateDid({ - did_id: did.launcher_id, - name: did.name, - visible: !did.visible, - }) - .then(updateDids) - .catch(addError); - }; - - const onTransferSubmit = (address: string, fee: string) => { - setIsTransferring(true); - setTransferAddress(address); - commands - .transferDids({ - did_ids: [did.launcher_id], - address, - fee: toMojos(fee, walletState.sync.unit.decimals), - }) - .then(setResponse) - .catch((err) => { - setIsTransferring(false); - addError(err); - }) - .finally(() => setTransferOpen(false)); - }; - - const onBurnSubmit = (fee: string) => { - setIsBurning(true); - commands - .transferDids({ - did_ids: [did.launcher_id], - address: walletState.sync.burn_address, - fee: toMojos(fee, walletState.sync.unit.decimals), - }) - .then(setResponse) - .catch((err) => { - setIsBurning(false); - addError(err); - }) - .finally(() => setBurnOpen(false)); - }; - - const onNormalizeSubmit = (fee: string) => { - setIsNormalizing(true); - commands - .normalizeDids({ - did_ids: [did.launcher_id], - fee: toMojos(fee, walletState.sync.unit.decimals), - }) - .then(setResponse) - .catch((err) => { - setIsNormalizing(false); - addError(err); - }) - .finally(() => setNormalizeOpen(false)); - }; - - return ( - <> - - - - - {did.name ?? t`Untitled Profile`} - - - - - - - - { - e.stopPropagation(); - setTransferOpen(true); - }} - disabled={did.created_height === null} - > - - - Transfer - - - - {did.recovery_hash === null && ( - { - e.stopPropagation(); - setNormalizeOpen(true); - }} - disabled={did.created_height === null} - > - - - Normalize - - - )} - - { - e.stopPropagation(); - setBurnOpen(true); - }} - disabled={did.created_height === null} - > - - - Burn - - - - - - { - e.stopPropagation(); - writeText(did.launcher_id); - toast.success(t`DID ID copied to clipboard`); - }} - > - - - Copy ID - - - - { - e.stopPropagation(); - setRenameOpen(true); - }} - > - - - Rename - - - - { - e.stopPropagation(); - toggleVisibility(); - }} - > - {did.visible ? ( - - ) : ( - - )} - {did.visible ? t`Hide` : t`Show`} - - - - - - -
{did.launcher_id}
-
-
- - !open && setRenameOpen(false)} - > - - - - Rename Profile - - - Enter the new display name for this profile. - - -
-
- - setName(event.target.value)} - onKeyDown={(event) => { - if (event.key === 'Enter') { - event.preventDefault(); - rename(); - } - }} - /> -
-
- - - - -
-
- - - This will send the profile to the provided address. - - - - - This will permanently delete the profile by sending it to the burn - address. - - - - - - This will modify the profile's recovery info to be compatible - with the Chia reference wallet. - - - - { - setResponse(null); - setIsTransferring(false); - setIsBurning(false); - setIsNormalizing(false); - }} - onConfirm={() => updateDids()} - additionalData={ - isTransferring && response - ? { - title: t`Transfer DID`, - content: ( - - ), - } - : isBurning && response - ? { - title: t`Burn DID`, - content: , - } - : isNormalizing && response - ? { - title: t`Normalize DID`, - content: , - } - : undefined - } - /> - - ); -} diff --git a/src/pages/Nft.tsx b/src/pages/Nft.tsx index bdd9e25c..43dae23c 100644 --- a/src/pages/Nft.tsx +++ b/src/pages/Nft.tsx @@ -1,10 +1,10 @@ import Container from '@/components/Container'; import { CopyBox } from '@/components/CopyBox'; import Header from '@/components/Header'; +import Profile from '@/components/Profile'; import { Button } from '@/components/ui/button'; import { useErrors } from '@/hooks/useErrors'; import spacescanLogo from '@/images/spacescan-logo-192.png'; -import { getMintGardenProfile } from '@/lib/marketplaces'; import { isAudio, isImage, isJson, isText, nftUri } from '@/lib/nftUri'; import { isValidUrl } from '@/lib/utils'; import { t } from '@lingui/core/macro'; @@ -77,36 +77,6 @@ export default function Nft() { .catch(addError); }, [addError]); - const [minterProfile, setMinterProfile] = useState<{ - encoded_id: string; - name: string; - avatar_uri: string | null; - } | null>(null); - - const [ownerProfile, setOwnerProfile] = useState<{ - encoded_id: string; - name: string; - avatar_uri: string | null; - } | null>(null); - - useEffect(() => { - if (!nft?.minter_did) { - setMinterProfile(null); - return; - } - - getMintGardenProfile(nft.minter_did).then(setMinterProfile); - }, [nft?.minter_did]); - - useEffect(() => { - if (!nft?.owner_did) { - setOwnerProfile(null); - return; - } - - getMintGardenProfile(nft.owner_did).then(setOwnerProfile); - }, [nft?.owner_did]); - return ( <>
@@ -319,21 +289,9 @@ export default function Nft() { value={nft?.minter_did ?? t`None`} onCopy={() => toast.success(t`Minter DID copied to clipboard`)} /> - {minterProfile && ( -
- openUrl(`https://mintgarden.io/${nft?.minter_did}`) - } - > - {minterProfile.avatar_uri && ( - {`${minterProfile.name} - )} -
{minterProfile.name}
+ {nft?.minter_did && ( +
+
)}
@@ -347,21 +305,9 @@ export default function Nft() { value={nft?.owner_did ?? t`None`} onCopy={() => toast.success(t`Owner DID copied to clipboard`)} /> - {ownerProfile && ( -
- openUrl(`https://mintgarden.io/${nft?.owner_did}`) - } - > - {ownerProfile.avatar_uri && ( - {`${ownerProfile.name} - )} -
{ownerProfile.name}
+ {nft?.owner_did && ( +
+
)}
From 7562176f45fd9b989b90e2187fead36f055747f3 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Wed, 30 Jul 2025 20:06:17 -0500 Subject: [PATCH 02/19] wording --- src/pages/DidList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/DidList.tsx b/src/pages/DidList.tsx index ec39cb39..7248d55b 100644 --- a/src/pages/DidList.tsx +++ b/src/pages/DidList.tsx @@ -54,7 +54,7 @@ export function DidList() { From cec2065e2bbbb11240c6c234519f1de899801e06 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Sat, 2 Aug 2025 15:20:49 -0500 Subject: [PATCH 03/19] adapt to AssetIcon change --- src/components/Profile.tsx | 59 +++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/src/components/Profile.tsx b/src/components/Profile.tsx index 440b0589..6540186f 100644 --- a/src/components/Profile.tsx +++ b/src/components/Profile.tsx @@ -45,7 +45,12 @@ import { } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { toast } from 'react-toastify'; -import { commands, DidRecord, TransactionResponse } from '../bindings'; +import { + AssetKind, + commands, + DidRecord, + TransactionResponse, +} from '../bindings'; import { CustomError } from '../contexts/ErrorContext'; export interface MintGardenProfile { @@ -227,12 +232,20 @@ function ProfileContent({ }; // Display logic - const displayName = !mintGardenProfile.is_unknown - ? mintGardenProfile.name - : didRecord?.name || mintGardenProfile?.name || t`Untitled Profile`; - - const displayDid = mintGardenProfile.encoded_id || ''; - + const didAsset = { + icon_url: mintGardenProfile.avatar_uri, + kind: 'did' as AssetKind, + revocation_address: null, + name: !mintGardenProfile.is_unknown + ? mintGardenProfile.name + : didRecord?.name || mintGardenProfile?.name || t`Untitled Profile`, + ticker: '', + precision: 0, + asset_id: mintGardenProfile.encoded_id || '', + balance: '0', + balanceInUsd: '0', + priceInUsd: '0', + }; // Loading state if (isMintGardenLoading) { return ( @@ -249,15 +262,11 @@ function ProfileContent({ if (variant === 'compact') { return (
- - {displayName} + + {didAsset.name} {showDid && ( - {displayDid} + {didAsset.asset_id} )} {!mintGardenProfile.is_unknown && ( @@ -266,7 +275,7 @@ function ProfileContent({ size='sm' onClick={handleMintGardenClick} className='p-1 h-auto' - title={`View ${displayName} on MintGarden`} + title={`View ${didAsset.name} on MintGarden`} > @@ -289,13 +298,8 @@ function ProfileContent({ > - - {displayName} + + {didAsset.name} {didRecord ? ( @@ -405,7 +409,9 @@ function ProfileContent({ ) : null} -
{displayDid}
+
+ {didAsset.asset_id} +
@@ -545,12 +551,12 @@ function ProfileContent({ return (
- +
-
{displayName}
+
{didAsset.name}
{showDid && (
- {displayDid} + {didAsset.asset_id}
)}
@@ -569,7 +575,6 @@ function ProfileContent({ ); } -// Enhanced Unified Profile Component export function Profile({ did, didRecord, From 55df792590549caaeb1dbd95e1f766af5729e742 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Sat, 2 Aug 2025 19:10:07 -0500 Subject: [PATCH 04/19] service to handle cache and rate limiting --- src/App.tsx | 6 ++ src/hooks/useMintGardenConfig.ts | 31 ++++++ src/lib/marketplaces.ts | 27 +----- src/lib/mintGardenConfig.ts | 28 ++++++ src/lib/mintGardenService.ts | 159 +++++++++++++++++++++++++++++++ 5 files changed, 226 insertions(+), 25 deletions(-) create mode 100644 src/hooks/useMintGardenConfig.ts create mode 100644 src/lib/mintGardenConfig.ts create mode 100644 src/lib/mintGardenService.ts diff --git a/src/App.tsx b/src/App.tsx index c364028a..dcfa2086 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,6 +26,7 @@ import { WalletConnectProvider } from './contexts/WalletConnectContext'; import { WalletProvider } from './contexts/WalletContext'; import useInitialization from './hooks/useInitialization'; import { useTransactionFailures } from './hooks/useTransactionFailures'; +import { initializeMintGardenService } from './lib/mintGardenConfig'; import { loadCatalog } from './i18n'; import Addresses from './pages/Addresses'; import CollectionMetaData from './pages/CollectionMetaData'; @@ -162,6 +163,11 @@ function AppInner() { // Enable global transaction failure handling useTransactionFailures(); + // Initialize MintGarden service with rate limiting configuration + useEffect(() => { + initializeMintGardenService(); + }, []); + useEffect(() => { const initLocale = async () => { await loadCatalog(locale); diff --git a/src/hooks/useMintGardenConfig.ts b/src/hooks/useMintGardenConfig.ts new file mode 100644 index 00000000..84f28d45 --- /dev/null +++ b/src/hooks/useMintGardenConfig.ts @@ -0,0 +1,31 @@ +import { useCallback } from 'react'; +import { mintGardenService } from '@/lib/mintGardenService'; +import type { MintGardenServiceConfig } from '@/lib/mintGardenService'; + +export function useMintGardenConfig() { + const getConfig = useCallback(() => { + return mintGardenService.getConfig(); + }, []); + + const updateConfig = useCallback( + (newConfig: Partial) => { + mintGardenService.updateConfig(newConfig); + }, + [], + ); + + const clearCache = useCallback(() => { + mintGardenService.clearCache(); + }, []); + + const clearExpiredCache = useCallback(() => { + mintGardenService.clearExpiredCache(); + }, []); + + return { + getConfig, + updateConfig, + clearCache, + clearExpiredCache, + }; +} diff --git a/src/lib/marketplaces.ts b/src/lib/marketplaces.ts index 7249117b..8331db81 100644 --- a/src/lib/marketplaces.ts +++ b/src/lib/marketplaces.ts @@ -55,28 +55,5 @@ export const marketplaces: MarketplaceConfig[] = [ }, ]; -export async function getMintGardenProfile(did: string) { - try { - const response = await fetch(`https://api.mintgarden.io/profile/${did}`); - const data = await response.json(); - if (data?.detail === 'Unknown profile.') { - return { - encoded_id: did, - name: `${did.slice(9, 19)}...${did.slice(-4)}`, // 9 strips off "did:chia:" - avatar_uri: null, - is_unknown: true, - }; - } - // always supply a name - data.name = data.name || `${did.slice(9, 19)}...${did.slice(-4)}`; - data.is_unknown = false; - return data; - } catch { - return { - encoded_id: did, - name: `${did.slice(9, 19)}...${did.slice(-4)}`, // 9 strips off "did:chia:" - avatar_uri: null, - is_unknown: true, - }; - } -} +// Re-export the rate-limited and cached version +export { getMintGardenProfile } from './mintGardenService'; diff --git a/src/lib/mintGardenConfig.ts b/src/lib/mintGardenConfig.ts new file mode 100644 index 00000000..fcf0e3c4 --- /dev/null +++ b/src/lib/mintGardenConfig.ts @@ -0,0 +1,28 @@ +import { mintGardenService } from './mintGardenService'; + +// MintGarden API rate limiting configuration +export const MINTGARDEN_CONFIG = { + // Delay between API requests in milliseconds + // Increase this value if you're getting 429 errors + DELAY_BETWEEN_REQUESTS: 1000, // 1 second + + // How long to cache profile data in milliseconds + // 5 minutes = 15 * 60 * 1000 + CACHE_DURATION: 15 * 60 * 1000, + + // Maximum number of concurrent requests + // Lower this value if you're getting 429 errors + MAX_CONCURRENT_REQUESTS: 3, +} as const; + +// Initialize the service with the configuration +export function initializeMintGardenService(): void { + mintGardenService.updateConfig({ + delayBetweenRequests: MINTGARDEN_CONFIG.DELAY_BETWEEN_REQUESTS, + cacheDuration: MINTGARDEN_CONFIG.CACHE_DURATION, + maxConcurrentRequests: MINTGARDEN_CONFIG.MAX_CONCURRENT_REQUESTS, + }); +} + +// Export the service for direct access if needed +export { mintGardenService }; diff --git a/src/lib/mintGardenService.ts b/src/lib/mintGardenService.ts new file mode 100644 index 00000000..d6ecbd4b --- /dev/null +++ b/src/lib/mintGardenService.ts @@ -0,0 +1,159 @@ +import { MintGardenProfile } from '@/components/Profile'; + +interface CacheEntry { + profile: MintGardenProfile; + timestamp: number; +} + +interface MintGardenServiceConfig { + delayBetweenRequests: number; // milliseconds + cacheDuration: number; // milliseconds + maxConcurrentRequests: number; +} + +class MintGardenService { + private cache = new Map(); + private pendingRequests = new Map>(); + private lastRequestTime = 0; + private config: MintGardenServiceConfig; + + constructor(config: Partial = {}) { + this.config = { + delayBetweenRequests: 1000, // 1 second default + cacheDuration: 15 * 60 * 1000, // 15 minutes default + maxConcurrentRequests: 3, // Allow 3 concurrent requests + ...config, + }; + } + + private async delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + private async ensureRateLimit(): Promise { + const now = Date.now(); + const timeSinceLastRequest = now - this.lastRequestTime; + + if (timeSinceLastRequest < this.config.delayBetweenRequests) { + const delayNeeded = + this.config.delayBetweenRequests - timeSinceLastRequest; + await this.delay(delayNeeded); + } + + this.lastRequestTime = Date.now(); + } + + private isCacheValid(entry: CacheEntry): boolean { + const now = Date.now(); + return now - entry.timestamp < this.config.cacheDuration; + } + + private async fetchProfileFromAPI(did: string): Promise { + await this.ensureRateLimit(); + + try { + const response = await fetch(`https://api.mintgarden.io/profile/${did}`); + const data = await response.json(); + + if (data?.detail === 'Unknown profile.') { + return { + encoded_id: did, + name: `${did.slice(9, 19)}...${did.slice(-4)}`, + avatar_uri: null, + is_unknown: true, + }; + } + + // always supply a name + data.name = data.name || `${did.slice(9, 19)}...${did.slice(-4)}`; + data.is_unknown = false; + return data; + } catch { + return { + encoded_id: did, + name: `${did.slice(9, 19)}...${did.slice(-4)}`, + avatar_uri: null, + is_unknown: true, + }; + } + } + + async getProfile(did: string): Promise { + // Check cache first + const cached = this.cache.get(did); + if (cached && this.isCacheValid(cached)) { + return cached.profile; + } + + // Check if there's already a pending request for this DID + const pendingRequest = this.pendingRequests.get(did); + if (pendingRequest) { + return pendingRequest; + } + + // Check concurrent request limit + if (this.pendingRequests.size >= this.config.maxConcurrentRequests) { + // Wait for any request to complete + await Promise.race(this.pendingRequests.values()); + // Recursive call to try again + return this.getProfile(did); + } + + // Create new request + const requestPromise = this.fetchProfileFromAPI(did).then((profile) => { + // Cache the result + this.cache.set(did, { + profile, + timestamp: Date.now(), + }); + + // Remove from pending requests + this.pendingRequests.delete(did); + + return profile; + }); + + // Add to pending requests + this.pendingRequests.set(did, requestPromise); + + return requestPromise; + } + + // Clear cache entries that have expired + clearExpiredCache(): void { + for (const [key, entry] of this.cache.entries()) { + if (!this.isCacheValid(entry)) { + this.cache.delete(key); + } + } + } + + // Clear all cache + clearCache(): void { + this.cache.clear(); + } + + // Update configuration + updateConfig(newConfig: Partial): void { + this.config = { ...this.config, ...newConfig }; + } + + // Get current configuration + getConfig(): MintGardenServiceConfig { + return { ...this.config }; + } +} + +// Create a singleton instance +export const mintGardenService = new MintGardenService(); + +// Export the function that maintains backward compatibility +export async function getMintGardenProfile( + did: string, +): Promise { + return mintGardenService.getProfile(did); +} + +// Export the service class and configuration interface for advanced usage +export { MintGardenService }; +export type { MintGardenServiceConfig }; From e65fc0684bf4803c0ca581a915bffb86f6cfa2d9 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Sat, 2 Aug 2025 19:34:09 -0500 Subject: [PATCH 05/19] use tauri plugin store for mintgarden cache --- Cargo.lock | 263 ++++++++++++++++++---------- package.json | 1 + pnpm-lock.yaml | 15 ++ src-tauri/Cargo.toml | 1 + src-tauri/capabilities/default.json | 5 +- src-tauri/src/lib.rs | 3 +- src/hooks/useMintGardenConfig.ts | 8 +- src/lib/mintGardenConfig.ts | 2 +- src/lib/mintGardenService.ts | 181 +++++++++++++++++-- 9 files changed, 366 insertions(+), 113 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3237842b..ecb77d28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -554,7 +554,7 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "itoa 1.0.15", + "itoa", "matchit", "memchr", "mime", @@ -850,9 +850,9 @@ dependencies = [ [[package]] name = "brotli" -version = "7.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -861,9 +861,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.3" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -1859,15 +1859,15 @@ dependencies = [ [[package]] name = "cssparser" -version = "0.27.2" +version = "0.29.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" dependencies = [ "cssparser-macros", "dtoa-short", - "itoa 0.4.8", + "itoa", "matches", - "phf 0.8.0", + "phf 0.10.1", "proc-macro2", "quote", "smallvec", @@ -3114,16 +3114,14 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.26.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", "mac", "markup5ever", - "proc-macro2", - "quote", - "syn 1.0.109", + "match_token", ] [[package]] @@ -3134,7 +3132,7 @@ checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", - "itoa 1.0.15", + "itoa", ] [[package]] @@ -3186,7 +3184,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.15", + "itoa", "pin-project-lite", "smallvec", "tokio", @@ -3569,12 +3567,6 @@ dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - [[package]] name = "itoa" version = "1.0.15" @@ -3710,14 +3702,13 @@ dependencies = [ [[package]] name = "kuchikiki" -version = "0.8.2" +version = "0.8.8-speedreader" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ "cssparser", "html5ever", - "indexmap 1.9.3", - "matches", + "indexmap 2.10.0", "selectors", ] @@ -3909,18 +3900,29 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "markup5ever" -version = "0.11.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" dependencies = [ "log", - "phf 0.10.1", - "phf_codegen 0.10.0", + "phf 0.11.3", + "phf_codegen 0.11.3", "string_cache", "string_cache_codegen", "tendril", ] +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "matchers" version = "0.1.0" @@ -4022,9 +4024,9 @@ dependencies = [ [[package]] name = "muda" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de14a9b5d569ca68d7c891d613b390cf5ab4f851c77aaa2f9e435555d3d9492" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" dependencies = [ "crossbeam-channel", "dpi", @@ -4038,7 +4040,7 @@ dependencies = [ "png", "serde", "thiserror 2.0.12", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -4691,9 +4693,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", "phf_shared 0.8.0", - "proc-macro-hack", ] [[package]] @@ -4702,7 +4702,9 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" dependencies = [ + "phf_macros 0.10.0", "phf_shared 0.10.0", + "proc-macro-hack", ] [[package]] @@ -4727,12 +4729,12 @@ dependencies = [ [[package]] name = "phf_codegen" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", + "phf_generator 0.11.3", + "phf_shared 0.11.3", ] [[package]] @@ -4767,12 +4769,12 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", + "phf_generator 0.10.0", + "phf_shared 0.10.0", "proc-macro-hack", "proc-macro2", "quote", @@ -5882,6 +5884,7 @@ dependencies = [ "tauri-plugin-safe-area-insets", "tauri-plugin-sage", "tauri-plugin-sharesheet", + "tauri-plugin-store", "tauri-plugin-window-state", "tauri-specta", "tokio", @@ -5976,22 +5979,20 @@ dependencies = [ [[package]] name = "selectors" -version = "0.22.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", "cssparser", "derive_more", "fxhash", "log", - "matches", "phf 0.8.0", "phf_codegen 0.8.0", "precomputed-hash", "servo_arc", "smallvec", - "thin-slice", ] [[package]] @@ -6051,7 +6052,7 @@ version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ - "itoa 1.0.15", + "itoa", "memchr", "ryu", "serde", @@ -6063,7 +6064,7 @@ version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" dependencies = [ - "itoa 1.0.15", + "itoa", "serde", ] @@ -6094,7 +6095,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.15", + "itoa", "ryu", "serde", ] @@ -6153,9 +6154,9 @@ dependencies = [ [[package]] name = "servo_arc" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" dependencies = [ "nodrop", "stable_deref_trait", @@ -6517,7 +6518,7 @@ dependencies = [ "hex", "hkdf", "hmac", - "itoa 1.0.15", + "itoa", "log", "md-5", "memchr", @@ -6556,7 +6557,7 @@ dependencies = [ "hkdf", "hmac", "home", - "itoa 1.0.15", + "itoa", "log", "md-5", "memchr", @@ -6734,9 +6735,9 @@ dependencies = [ [[package]] name = "tao" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82" +checksum = "49c380ca75a231b87b6c9dd86948f035012e7171d1a7c40a9c2890489a7ffd8a" dependencies = [ "bitflags 2.9.0", "core-foundation", @@ -6802,17 +6803,16 @@ checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "tauri" -version = "2.5.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7b0bc1aec81bda6bc455ea98fcaed26b3c98c1648c627ad6ff1c704e8bf8cbc" +checksum = "352a4bc7bf6c25f5624227e3641adf475a6535707451b09bb83271df8b7a6ac7" dependencies = [ "anyhow", "bytes", "dirs 6.0.0", "dunce", "embed_plist", - "futures-util", - "getrandom 0.2.16", + "getrandom 0.3.2", "glob", "gtk", "heck 0.5.0", @@ -6854,9 +6854,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a0350f0df1db385ca5c02888a83e0e66655c245b7443db8b78a70da7d7f8fc" +checksum = "182d688496c06bf08ea896459bf483eb29cdff35c1c4c115fb14053514303064" dependencies = [ "anyhow", "cargo_toml", @@ -6876,9 +6876,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93f035551bf7b11b3f51ad9bc231ebbe5e085565527991c16cf326aa38cdf47" +checksum = "b54a99a6cd8e01abcfa61508177e6096a4fe2681efecee9214e962f2f073ae4a" dependencies = [ "base64 0.22.1", "brotli", @@ -6903,9 +6903,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.2.0" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8db4df25e2d9d45de0c4c910da61cd5500190da14ae4830749fee3466dddd112" +checksum = "7945b14dc45e23532f2ded6e120170bbdd4af5ceaa45784a6b33d250fbce3f9e" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -6917,9 +6917,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a5ebe6a610d1b78a94650896e6f7c9796323f408800cef436e0fa0539de601" +checksum = "5bd5c1e56990c70a906ef67a9851bbdba9136d26075ee9a2b19c8b46986b3e02" dependencies = [ "anyhow", "glob", @@ -7094,6 +7094,22 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "tauri-plugin-store" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5916c609664a56c82aeaefffca9851fd072d4d41f73d63f22ee3ee451508194f" +dependencies = [ + "dunce", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.12", + "tokio", + "tracing", +] + [[package]] name = "tauri-plugin-window-state" version = "2.2.2" @@ -7111,9 +7127,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f004905d549854069e6774533d742b03cacfd6f03deb08940a8677586cbe39" +checksum = "2b1cc885be806ea15ff7b0eb47098a7b16323d9228876afda329e34e2d6c4676" dependencies = [ "cookie", "dpi", @@ -7133,9 +7149,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.6.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f85d056f4d4b014fe874814034f3416d57114b617a493a4fe552580851a3f3a2" +checksum = "fe653a2fbbef19fe898efc774bc52c8742576342a33d3d028c189b57eb1d2439" dependencies = [ "gtk", "http", @@ -7188,9 +7204,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2900399c239a471bcff7f15c4399eb1a8c4fe511ba2853e07c996d771a5e0a4" +checksum = "9330c15cabfe1d9f213478c9e8ec2b0c76dab26bb6f314b8ad1c8a568c1d186e" dependencies = [ "anyhow", "brotli", @@ -7281,12 +7297,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "thin-slice" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" - [[package]] name = "thiserror" version = "1.0.69" @@ -7364,7 +7374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", - "itoa 1.0.15", + "itoa", "num-conv", "powerfmt", "serde", @@ -7679,9 +7689,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.20.1" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7eee98ec5c90daf179d55c20a49d8c0d043054ce7c26336c09a24d31f14fa0" +checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2" dependencies = [ "crossbeam-channel", "dirs 6.0.0", @@ -8266,9 +8276,9 @@ dependencies = [ [[package]] name = "webview2-com" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b542b5cfbd9618c46c2784e4d41ba218c336ac70d44c55e47b251033e7d85601" +checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" dependencies = [ "webview2-com-macros", "webview2-com-sys", @@ -8291,9 +8301,9 @@ dependencies = [ [[package]] name = "webview2-com-sys" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae2d11c4a686e4409659d7891791254cf9286d3cfe0eef54df1523533d22295" +checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" dependencies = [ "thiserror 2.0.12", "windows 0.61.1", @@ -8586,6 +8596,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -8625,13 +8644,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows-version" version = "0.1.4" @@ -8659,6 +8694,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -8677,6 +8718,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -8695,12 +8742,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -8719,6 +8778,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -8737,6 +8802,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -8755,6 +8826,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -8773,6 +8850,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.5.40" @@ -8843,9 +8926,9 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "wry" -version = "0.51.2" +version = "0.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c886a0a9d2a94fd90cfa1d929629b79cfefb1546e2c7430c63a47f0664c0e4e2" +checksum = "12a714d9ba7075aae04a6e50229d6109e3d584774b99a6a8c60de1698ca111b9" dependencies = [ "base64 0.22.1", "block2 0.6.1", diff --git a/package.json b/package.json index 9f3162bc..7ce1d98b 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@tauri-apps/plugin-fs": "~2", "@tauri-apps/plugin-opener": "^2.2.6", "@tauri-apps/plugin-os": "^2.2.1", + "@tauri-apps/plugin-store": "^2.3.0", "@use-gesture/react": "^10.3.1", "@walletconnect/sign-client": "^2.17.2", "@walletconnect/types": "^2.17.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70368c41..a56b0a28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: '@tauri-apps/plugin-os': specifier: ^2.2.1 version: 2.2.1 + '@tauri-apps/plugin-store': + specifier: ^2.3.0 + version: 2.3.0 '@use-gesture/react': specifier: ^10.3.1 version: 10.3.1(react@18.3.1) @@ -1696,6 +1699,9 @@ packages: '@tauri-apps/api@2.3.0': resolution: {integrity: sha512-33Z+0lX2wgZbx1SPFfqvzI6su63hCBkbzv+5NexeYjIx7WA9htdOKoRR7Dh3dJyltqS5/J8vQFyybiRoaL0hlA==} + '@tauri-apps/api@2.7.0': + resolution: {integrity: sha512-v7fVE8jqBl8xJFOcBafDzXFc8FnicoH3j8o8DNNs0tHuEBmXUDqrCOAzMRX0UkfpwqZLqvrvK0GNQ45DfnoVDg==} + '@tauri-apps/cli-darwin-arm64@2.3.1': resolution: {integrity: sha512-TOhSdsXYt+f+asRU+Dl+Wufglj/7+CX9h8RO4hl5k7D6lR4L8yTtdhpS7btaclOMmjYC4piNfJE70GoxhOoYWw==} engines: {node: '>= 10'} @@ -1782,6 +1788,9 @@ packages: '@tauri-apps/plugin-os@2.2.1': resolution: {integrity: sha512-cNYpNri2CCc6BaNeB6G/mOtLvg8dFyFQyCUdf2y0K8PIAKGEWdEcu8DECkydU2B+oj4OJihDPD2de5K6cbVl9A==} + '@tauri-apps/plugin-store@2.3.0': + resolution: {integrity: sha512-mre8er0nXPhyEWQzWCpUd+UnEoBQYcoA5JYlwpwOV9wcxKqlXTGfminpKsE37ic8NUb2BIZqf0QQ9/U3ib2+/A==} + '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -5670,6 +5679,8 @@ snapshots: '@tauri-apps/api@2.3.0': {} + '@tauri-apps/api@2.7.0': {} + '@tauri-apps/cli-darwin-arm64@2.3.1': optional: true @@ -5741,6 +5752,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.3.0 + '@tauri-apps/plugin-store@2.3.0': + dependencies: + '@tauri-apps/api': 2.7.0 + '@types/estree@1.0.5': {} '@types/istanbul-lib-coverage@2.0.6': {} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5f034d62..4fa66d84 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -41,6 +41,7 @@ reqwest = { workspace = true } # https://aws.github.io/aws-lc-rs/platform_support.html#tested-platforms aws-lc-rs = { version = "1", features = ["bindgen"] } tauri-plugin-sharesheet = "0.0.1" +tauri-plugin-store = "2.3.0" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-window-state = { workspace = true } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 67c5e4fe..427545a4 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -14,6 +14,9 @@ "clipboard-manager:default", "clipboard-manager:allow-write-text", "clipboard-manager:allow-read-text", - "opener:default" + "opener:default", + "store:default", + "store:allow-load", + "store:allow-save" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index aba64fd9..83dde696 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -140,7 +140,8 @@ pub fn run() { let mut tauri_builder = tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_clipboard_manager::init()) - .plugin(tauri_plugin_os::init()); + .plugin(tauri_plugin_os::init()) + .plugin(tauri_plugin_store::Builder::new().build()); #[cfg(not(mobile))] { diff --git a/src/hooks/useMintGardenConfig.ts b/src/hooks/useMintGardenConfig.ts index 84f28d45..be97419a 100644 --- a/src/hooks/useMintGardenConfig.ts +++ b/src/hooks/useMintGardenConfig.ts @@ -14,12 +14,12 @@ export function useMintGardenConfig() { [], ); - const clearCache = useCallback(() => { - mintGardenService.clearCache(); + const clearCache = useCallback(async () => { + await mintGardenService.clearCache(); }, []); - const clearExpiredCache = useCallback(() => { - mintGardenService.clearExpiredCache(); + const clearExpiredCache = useCallback(async () => { + await mintGardenService.clearExpiredCache(); }, []); return { diff --git a/src/lib/mintGardenConfig.ts b/src/lib/mintGardenConfig.ts index fcf0e3c4..4deaa24f 100644 --- a/src/lib/mintGardenConfig.ts +++ b/src/lib/mintGardenConfig.ts @@ -7,7 +7,7 @@ export const MINTGARDEN_CONFIG = { DELAY_BETWEEN_REQUESTS: 1000, // 1 second // How long to cache profile data in milliseconds - // 5 minutes = 15 * 60 * 1000 + // 15 minutes = 15 * 60 * 1000 CACHE_DURATION: 15 * 60 * 1000, // Maximum number of concurrent requests diff --git a/src/lib/mintGardenService.ts b/src/lib/mintGardenService.ts index d6ecbd4b..2745ff66 100644 --- a/src/lib/mintGardenService.ts +++ b/src/lib/mintGardenService.ts @@ -1,4 +1,5 @@ import { MintGardenProfile } from '@/components/Profile'; +import { Store, load } from '@tauri-apps/plugin-store'; interface CacheEntry { profile: MintGardenProfile; @@ -12,10 +13,11 @@ interface MintGardenServiceConfig { } class MintGardenService { - private cache = new Map(); + private store: Store | null = null; private pendingRequests = new Map>(); private lastRequestTime = 0; private config: MintGardenServiceConfig; + private isInitialized = false; constructor(config: Partial = {}) { this.config = { @@ -24,6 +26,20 @@ class MintGardenService { maxConcurrentRequests: 3, // Allow 3 concurrent requests ...config, }; + + this.initializeStore(); + } + + private async initializeStore(): Promise { + if (this.isInitialized) return; + + try { + this.store = await load('.mintgarden-cache.dat'); + this.isInitialized = true; + } catch (error) { + console.warn('Failed to load MintGarden cache store:', error); + this.isInitialized = true; // Continue anyway + } } private async delay(ms: number): Promise { @@ -48,6 +64,45 @@ class MintGardenService { return now - entry.timestamp < this.config.cacheDuration; } + private async getCachedProfile(did: string): Promise { + await this.initializeStore(); + + if (!this.store) { + return null; + } + + try { + const cached = await this.store.get(`profile:${did}`); + + if (cached && this.isCacheValid(cached)) { + return cached.profile; + } + } catch (error) { + console.warn('Failed to read from cache:', error); + } + + return null; + } + + private async setCachedProfile(did: string, profile: MintGardenProfile): Promise { + await this.initializeStore(); + + if (!this.store) { + return; + } + + try { + const entry: CacheEntry = { + profile, + timestamp: Date.now(), + }; + await this.store.set(`profile:${did}`, entry); + await this.store.save(); + } catch (error) { + console.warn('Failed to write to cache:', error); + } + } + private async fetchProfileFromAPI(did: string): Promise { await this.ensureRateLimit(); @@ -80,9 +135,9 @@ class MintGardenService { async getProfile(did: string): Promise { // Check cache first - const cached = this.cache.get(did); - if (cached && this.isCacheValid(cached)) { - return cached.profile; + const cached = await this.getCachedProfile(did); + if (cached) { + return cached; } // Check if there's already a pending request for this DID @@ -98,14 +153,11 @@ class MintGardenService { // Recursive call to try again return this.getProfile(did); } - + // Create new request - const requestPromise = this.fetchProfileFromAPI(did).then((profile) => { + const requestPromise = this.fetchProfileFromAPI(did).then(async (profile) => { // Cache the result - this.cache.set(did, { - profile, - timestamp: Date.now(), - }); + await this.setCachedProfile(did, profile); // Remove from pending requests this.pendingRequests.delete(did); @@ -120,17 +172,114 @@ class MintGardenService { } // Clear cache entries that have expired - clearExpiredCache(): void { - for (const [key, entry] of this.cache.entries()) { - if (!this.isCacheValid(entry)) { - this.cache.delete(key); + async clearExpiredCache(): Promise { + await this.initializeStore(); + + if (!this.store) { + return; + } + + try { + const keys = await this.store.keys(); + const profileKeys = keys.filter(key => key.startsWith('profile:')); + + for (const key of profileKeys) { + const entry = await this.store.get(key); + if (entry && !this.isCacheValid(entry)) { + await this.store.delete(key); + } } + + await this.store.save(); + } catch (error) { + console.warn('Failed to clear expired cache:', error); } } // Clear all cache - clearCache(): void { - this.cache.clear(); + async clearCache(): Promise { + await this.initializeStore(); + + if (!this.store) { + return; + } + + try { + const keys = await this.store.keys(); + const profileKeys = keys.filter(key => key.startsWith('profile:')); + + for (const key of profileKeys) { + await this.store.delete(key); + } + + await this.store.save(); + } catch (error) { + console.warn('Failed to clear cache:', error); + } + } + + // Get cache statistics + async getCacheStats(): Promise<{ total: number; valid: number; expired: number }> { + await this.initializeStore(); + + if (!this.store) { + return { total: 0, valid: 0, expired: 0 }; + } + + try { + const keys = await this.store.keys(); + const profileKeys = keys.filter(key => key.startsWith('profile:')); + + let valid = 0; + let expired = 0; + + for (const key of profileKeys) { + const entry = await this.store.get(key); + if (entry) { + if (this.isCacheValid(entry)) { + valid++; + } else { + expired++; + } + } + } + + return { + total: profileKeys.length, + valid, + expired, + }; + } catch (error) { + console.warn('Failed to get cache stats:', error); + return { total: 0, valid: 0, expired: 0 }; + } + } + + // Debug method to check if a specific DID is cached + async isCached(did: string): Promise { + const cached = await this.getCachedProfile(did); + return cached !== null; + } + + // Debug method to get cache details for a specific DID + async getCacheDetails(did: string): Promise<{ cached: boolean; entry?: CacheEntry; valid: boolean }> { + await this.initializeStore(); + + if (!this.store) { + return { cached: false, valid: false }; + } + + try { + const entry = await this.store.get(`profile:${did}`); + if (entry) { + const valid = this.isCacheValid(entry); + return { cached: true, entry, valid }; + } + return { cached: false, valid: false }; + } catch (error) { + console.warn('Failed to get cache details:', error); + return { cached: false, valid: false }; + } } // Update configuration From 35cc740281cde40ec2cad5ff340928423948f830 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Sun, 3 Aug 2025 11:03:05 -0500 Subject: [PATCH 06/19] re-enable mintgarden profile on nft minter view --- Cargo.lock | 2 +- src/components/NftGroupCard.tsx | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aa084f1f..e5d3b7c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1878,7 +1878,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" dependencies = [ "csv-core", - "itoa 1.0.15", + "itoa", "ryu", "serde", ] diff --git a/src/components/NftGroupCard.tsx b/src/components/NftGroupCard.tsx index f820ba39..96a23602 100644 --- a/src/components/NftGroupCard.tsx +++ b/src/components/NftGroupCard.tsx @@ -7,7 +7,7 @@ import { import { MintGardenProfile } from '@/components/Profile'; import { NftGroupMode } from '@/hooks/useNftParams'; import useOfferStateWithDefault from '@/hooks/useOfferStateWithDefault'; -//import { getMintGardenProfile } from '@/lib/marketplaces'; +import { getMintGardenProfile } from '@/lib/marketplaces'; import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { writeText } from '@tauri-apps/plugin-clipboard-manager'; @@ -78,12 +78,7 @@ export function NftGroupCard({ // Fetch profile data for DID cards useEffect(() => { if (!isCollection && isDidRecord(item)) { - setDidProfile({ - encoded_id: item.launcher_id, - name: item.name || item.launcher_id.slice(0, 8), - avatar_uri: null, - }); - //getMintGardenProfile(item.launcher_id).then(setDidProfile); + getMintGardenProfile(item.launcher_id).then(setDidProfile); } }, [isCollection, item]); // Type guards to help TypeScript narrow the types From 852eeef662393bba284b144bcd3d218b8faf1f02 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Sun, 3 Aug 2025 14:18:49 -0500 Subject: [PATCH 07/19] simplify card view --- src/components/Profile.tsx | 419 +++++++++++------------------------ src/lib/mintGardenService.ts | 89 ++++---- src/pages/DidList.tsx | 4 +- 3 files changed, 180 insertions(+), 332 deletions(-) diff --git a/src/components/Profile.tsx b/src/components/Profile.tsx index 6540186f..9984adb6 100644 --- a/src/components/Profile.tsx +++ b/src/components/Profile.tsx @@ -26,7 +26,7 @@ import { Label } from '@/components/ui/label'; import { Skeleton } from '@/components/ui/skeleton'; import { useErrors } from '@/hooks/useErrors'; import { getMintGardenProfile } from '@/lib/marketplaces'; -import { toMojos } from '@/lib/utils'; +import { getAssetDisplayName, toMojos } from '@/lib/utils'; import { useWalletState } from '@/state'; import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; @@ -43,7 +43,7 @@ import { PenIcon, SendIcon, } from 'lucide-react'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { AssetKind, @@ -62,87 +62,118 @@ export interface MintGardenProfile { export interface ProfileProps { // For MintGarden profiles (DID string only) - did?: string; - // For local DID records (with enhanced MintGarden data) - didRecord?: DidRecord; - // Common props + did: DidRecord | string; variant?: 'default' | 'compact' | 'card'; - showDid?: boolean; className?: string; - onProfileLoad?: (profile: MintGardenProfile) => void; - // Local DID specific updateDids?: () => void; - showMintGardenProfile?: boolean; + allowMintGardenProfile?: boolean; } -// ProfileContent component to handle the actual rendering -interface ProfileContentProps { - didRecord?: DidRecord; - mintGardenProfile: MintGardenProfile; - isMintGardenLoading: boolean; - variant: 'default' | 'compact' | 'card'; - showDid: boolean; - className: string; - showMintGardenProfile: boolean; - name: string; - setName: (name: string) => void; - response: TransactionResponse | null; - renameOpen: boolean; - setRenameOpen: (open: boolean) => void; - transferOpen: boolean; - setTransferOpen: (open: boolean) => void; - burnOpen: boolean; - setBurnOpen: (open: boolean) => void; - normalizeOpen: boolean; - setNormalizeOpen: (open: boolean) => void; - isTransferring: boolean; - isBurning: boolean; - isNormalizing: boolean; - transferAddress: string; - updateDids?: () => void; - addError: (error: CustomError) => void; - walletState: { sync: { unit: { decimals: number }; burn_address: string } }; - setResponse: (response: TransactionResponse | null) => void; - setIsTransferring: (transferring: boolean) => void; - setIsBurning: (burning: boolean) => void; - setIsNormalizing: (normalizing: boolean) => void; - setTransferAddress: (address: string) => void; -} - -function ProfileContent({ - didRecord, - mintGardenProfile, - isMintGardenLoading, - variant, - showDid, - className, - name, - setName, - response, - renameOpen, - setRenameOpen, - transferOpen, - setTransferOpen, - burnOpen, - setBurnOpen, - normalizeOpen, - setNormalizeOpen, - isTransferring, - isBurning, - isNormalizing, - transferAddress, +export function Profile({ + did, + variant = 'default', + className = '', + allowMintGardenProfile: allowMintGardenProfile = true, updateDids, - addError, - walletState, - setResponse, - setIsTransferring, - setIsBurning, - setIsNormalizing, - setTransferAddress, -}: ProfileContentProps) { - // Local DID action handlers +}: ProfileProps) { + const { addError } = useErrors(); + const walletState = useWalletState(); + + // this component can be used to display a DID record or a DID string + // if it is a DID DidRecord, it will be treated as a local DID and will have + // additional actions available + const isOwned = typeof did !== 'string'; + const didRecord: DidRecord = + typeof did === 'string' + ? { + launcher_id: did, + name: `${did.slice(9, 19)}...${did.slice(-4)}`, + visible: true, + created_height: null, + recovery_hash: null, + coin_id: '0', + address: '', + amount: 0, + } + : did; + + const [mintGardenProfile, setMintGardenProfile] = useState( + { + encoded_id: didRecord.launcher_id, + name: didRecord.name ?? '', + avatar_uri: null, + is_unknown: true, + }, + ); + + const didAsset = { + icon_url: mintGardenProfile.avatar_uri, + kind: 'did' as AssetKind, + revocation_address: null, + name: !mintGardenProfile.is_unknown + ? mintGardenProfile.name + : getAssetDisplayName( + didRecord?.name || mintGardenProfile?.name, + null, + 'did', + ), + ticker: '', + precision: 0, + asset_id: didRecord.launcher_id, + balance: '0', + balanceInUsd: '0', + priceInUsd: '0', + }; + + const [isMintGardenLoading, setIsMintGardenLoading] = useState(false); + + // State for local DID actions (only when did is owned) + const [name, setName] = useState(''); + const [response, setResponse] = useState(null); + const [renameOpen, setRenameOpen] = useState(false); + const [transferOpen, setTransferOpen] = useState(false); + const [burnOpen, setBurnOpen] = useState(false); + const [normalizeOpen, setNormalizeOpen] = useState(false); + const [isTransferring, setIsTransferring] = useState(false); + const [isBurning, setIsBurning] = useState(false); + const [isNormalizing, setIsNormalizing] = useState(false); + const [transferAddress, setTransferAddress] = useState(''); + + // Fetch MintGarden profile data + useEffect(() => { + if (!didRecord.launcher_id) return; + + if (allowMintGardenProfile) { + setIsMintGardenLoading(true); + getMintGardenProfile(didRecord.launcher_id) + .then((profileData) => { + setMintGardenProfile(profileData); + }) + .catch(() => { + // Create fallback profile for failed lookups + setMintGardenProfile({ + encoded_id: didRecord.launcher_id, + name: didRecord.name ?? '', + avatar_uri: null, + is_unknown: true, + }); + }) + .finally(() => { + setIsMintGardenLoading(false); + }); + } else { + setMintGardenProfile({ + encoded_id: didRecord.launcher_id, + name: didRecord.name ?? '', + avatar_uri: null, + is_unknown: true, + }); + } + }, [didRecord.launcher_id, didRecord.name, allowMintGardenProfile]); + + // Owned DID action handlers const rename = () => { - if (!name || !didRecord) return; + if (!name || !isOwned) return; commands .updateDid({ @@ -159,7 +190,7 @@ function ProfileContent({ }; const toggleVisibility = () => { - if (!didRecord) return; + if (!isOwned) return; commands .updateDid({ @@ -172,7 +203,7 @@ function ProfileContent({ }; const onTransferSubmit = (address: string, fee: string) => { - if (!didRecord) return; + if (!isOwned) return; setIsTransferring(true); setTransferAddress(address); @@ -191,7 +222,7 @@ function ProfileContent({ }; const onBurnSubmit = (fee: string) => { - if (!didRecord) return; + if (!isOwned) return; setIsBurning(true); commands @@ -209,7 +240,7 @@ function ProfileContent({ }; const onNormalizeSubmit = (fee: string) => { - if (!didRecord) return; + if (!isOwned) return; setIsNormalizing(true); commands @@ -231,21 +262,6 @@ function ProfileContent({ } }; - // Display logic - const didAsset = { - icon_url: mintGardenProfile.avatar_uri, - kind: 'did' as AssetKind, - revocation_address: null, - name: !mintGardenProfile.is_unknown - ? mintGardenProfile.name - : didRecord?.name || mintGardenProfile?.name || t`Untitled Profile`, - ticker: '', - precision: 0, - asset_id: mintGardenProfile.encoded_id || '', - balance: '0', - balanceInUsd: '0', - priceInUsd: '0', - }; // Loading state if (isMintGardenLoading) { return ( @@ -253,7 +269,6 @@ function ProfileContent({
- {showDid && }
); @@ -264,11 +279,7 @@ function ProfileContent({
{didAsset.name} - {showDid && ( - - {didAsset.asset_id} - - )} + {!mintGardenProfile.is_unknown && (
From 60092ceceb12b1ed1399372623211571162a3520 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Sun, 3 Aug 2025 14:21:28 -0500 Subject: [PATCH 08/19] rename Profile to ProfileCard --- src/components/NftGroupCard.tsx | 2 +- src/components/{Profile.tsx => ProfileCard.tsx} | 8 ++++---- src/lib/mintGardenService.ts | 3 ++- src/pages/CollectionMetaData.tsx | 4 ++-- src/pages/DidList.tsx | 4 ++-- src/pages/Nft.tsx | 6 +++--- 6 files changed, 14 insertions(+), 13 deletions(-) rename src/components/{Profile.tsx => ProfileCard.tsx} (99%) diff --git a/src/components/NftGroupCard.tsx b/src/components/NftGroupCard.tsx index 96a23602..46e1b10f 100644 --- a/src/components/NftGroupCard.tsx +++ b/src/components/NftGroupCard.tsx @@ -4,7 +4,7 @@ import { NftRecord, commands, } from '@/bindings'; -import { MintGardenProfile } from '@/components/Profile'; +import { MintGardenProfile } from '@/components/ProfileCard'; import { NftGroupMode } from '@/hooks/useNftParams'; import useOfferStateWithDefault from '@/hooks/useOfferStateWithDefault'; import { getMintGardenProfile } from '@/lib/marketplaces'; diff --git a/src/components/Profile.tsx b/src/components/ProfileCard.tsx similarity index 99% rename from src/components/Profile.tsx rename to src/components/ProfileCard.tsx index 9984adb6..98845d87 100644 --- a/src/components/Profile.tsx +++ b/src/components/ProfileCard.tsx @@ -60,7 +60,7 @@ export interface MintGardenProfile { is_unknown: boolean; } -export interface ProfileProps { +export interface ProfileCardProps { // For MintGarden profiles (DID string only) did: DidRecord | string; variant?: 'default' | 'compact' | 'card'; @@ -69,13 +69,13 @@ export interface ProfileProps { allowMintGardenProfile?: boolean; } -export function Profile({ +export function ProfileCard({ did, variant = 'default', className = '', allowMintGardenProfile: allowMintGardenProfile = true, updateDids, -}: ProfileProps) { +}: ProfileCardProps) { const { addError } = useErrors(); const walletState = useWalletState(); @@ -591,4 +591,4 @@ export function Profile({ ); } -export default Profile; +export default ProfileCard; diff --git a/src/lib/mintGardenService.ts b/src/lib/mintGardenService.ts index 0dfeda63..5a7e64c0 100644 --- a/src/lib/mintGardenService.ts +++ b/src/lib/mintGardenService.ts @@ -1,4 +1,4 @@ -import { MintGardenProfile } from '@/components/Profile'; +import { MintGardenProfile } from '@/components/ProfileCard'; import { Store, load } from '@tauri-apps/plugin-store'; interface CacheEntry { @@ -319,3 +319,4 @@ export async function getMintGardenProfile( // Export the service class and configuration interface for advanced usage export { MintGardenService }; export type { MintGardenServiceConfig }; + diff --git a/src/pages/CollectionMetaData.tsx b/src/pages/CollectionMetaData.tsx index 29cc943b..362af636 100644 --- a/src/pages/CollectionMetaData.tsx +++ b/src/pages/CollectionMetaData.tsx @@ -2,7 +2,7 @@ import { commands, NetworkKind, NftCollectionRecord } from '@/bindings'; import Container from '@/components/Container'; import { CopyBox } from '@/components/CopyBox'; import Header from '@/components/Header'; -import Profile, { MintGardenProfile } from '@/components/Profile'; +import ProfileCard, { MintGardenProfile } from '@/components/ProfileCard'; import { Button } from '@/components/ui/button'; import { CustomError } from '@/contexts/ErrorContext'; import { useErrors } from '@/hooks/useErrors'; @@ -379,7 +379,7 @@ export default function CollectionMetaData() { /> {minterProfile && (
- +
)}
diff --git a/src/pages/DidList.tsx b/src/pages/DidList.tsx index 047876b1..e451aad2 100644 --- a/src/pages/DidList.tsx +++ b/src/pages/DidList.tsx @@ -1,6 +1,6 @@ import Container from '@/components/Container'; import Header from '@/components/Header'; -import { Profile } from '@/components/Profile'; +import { ProfileCard } from '@/components/ProfileCard'; import { ReceiveAddress } from '@/components/ReceiveAddress'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; @@ -63,7 +63,7 @@ export function DidList() {
{visibleDids.map((did) => ( - {nft?.minter_did && (
- +
)}
@@ -307,7 +307,7 @@ export default function Nft() { /> {nft?.owner_did && (
- +
)} From 20e421cbe5fb6c45c92e77fc9f84cbf2691c81d1 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Sun, 3 Aug 2025 17:06:56 -0500 Subject: [PATCH 09/19] profile page --- crates/sage-api/src/requests/data.rs | 12 + crates/sage-database/src/tables/assets/did.rs | 67 ++- crates/sage/src/endpoints/data.rs | 34 +- src/pages/Profile.tsx | 392 ++++++++++++++++++ 4 files changed, 496 insertions(+), 9 deletions(-) create mode 100644 src/pages/Profile.tsx diff --git a/crates/sage-api/src/requests/data.rs b/crates/sage-api/src/requests/data.rs index c693d0d6..71497d3c 100644 --- a/crates/sage-api/src/requests/data.rs +++ b/crates/sage-api/src/requests/data.rs @@ -210,6 +210,18 @@ pub struct GetTokenResponse { pub token: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +pub struct GetProfile { + pub launcher_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +pub struct GetProfileResponse { + pub did: Option, +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct GetDids {} diff --git a/crates/sage-database/src/tables/assets/did.rs b/crates/sage-database/src/tables/assets/did.rs index 925200a4..990b5c70 100644 --- a/crates/sage-database/src/tables/assets/did.rs +++ b/crates/sage-database/src/tables/assets/did.rs @@ -18,6 +18,67 @@ pub struct DidRow { } impl Database { + pub async fn owned_did(&self, launcher_id: String) -> Result> { + let hash = launcher_id.as_ref(); + + query!( + " + SELECT + asset_hash, asset_name, asset_ticker, asset_precision, asset_icon_url, + asset_description, asset_is_visible, asset_is_sensitive_content, + asset_hidden_puzzle_hash, owned_coins.created_height, spent_height, + parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash, + metadata, recovery_list_hash, num_verifications_required, + offer_hash AS 'offer_hash?', created_timestamp, spent_timestamp, + clawback_expiration_seconds AS 'clawback_timestamp?' + FROM owned_coins + INNER JOIN dids ON dids.asset_id = owned_coins.asset_id + WHERE owned_coins.asset_hash = ? + ", + hash + ) + .fetch_optional(&self.pool) + .await? + .map(|row| { + Ok(DidRow { + asset: Asset { + hash: row.asset_hash.convert()?, + name: row.asset_name, + ticker: row.asset_ticker, + precision: row.asset_precision.convert()?, + icon_url: row.asset_icon_url, + description: row.asset_description, + is_visible: row.asset_is_visible, + is_sensitive_content: row.asset_is_sensitive_content, + hidden_puzzle_hash: row.asset_hidden_puzzle_hash.convert()?, + kind: AssetKind::Did, + }, + did_info: DidCoinInfo { + metadata: row.metadata.into(), + recovery_list_hash: row.recovery_list_hash.convert()?, + num_verifications_required: row.num_verifications_required.convert()?, + }, + coin_row: CoinRow { + coin: Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ), + p2_puzzle_hash: row.p2_puzzle_hash.convert()?, + kind: CoinKind::Did, + mempool_item_hash: None, + offer_hash: row.offer_hash.convert()?, + clawback_timestamp: row.clawback_timestamp.map(|t| t as u64), + created_height: row.created_height.convert()?, + spent_height: row.spent_height.convert()?, + created_timestamp: row.created_timestamp.convert()?, + spent_timestamp: row.spent_timestamp.convert()?, + }, + }) + }) + .transpose()? + } + pub async fn owned_dids(&self) -> Result> { query!( " @@ -27,8 +88,8 @@ impl Database { asset_hidden_puzzle_hash, owned_coins.created_height, spent_height, parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash, metadata, recovery_list_hash, num_verifications_required, - offer_hash, created_timestamp, spent_timestamp, - clawback_expiration_seconds AS clawback_timestamp + offer_hash AS 'offer_hash?', created_timestamp, spent_timestamp, + clawback_expiration_seconds AS 'clawback_timestamp?' FROM owned_coins INNER JOIN dids ON dids.asset_id = owned_coins.asset_id ORDER BY asset_name ASC @@ -66,7 +127,7 @@ impl Database { kind: CoinKind::Did, mempool_item_hash: None, offer_hash: row.offer_hash.convert()?, - clawback_timestamp: row.clawback_timestamp.convert()?, + clawback_timestamp: row.clawback_timestamp.map(|t| t as u64), created_height: row.created_height.convert()?, spent_height: row.spent_height.convert()?, created_timestamp: row.created_timestamp.convert()?, diff --git a/crates/sage/src/endpoints/data.rs b/crates/sage/src/endpoints/data.rs index fa40e7f4..284e7e11 100644 --- a/crates/sage/src/endpoints/data.rs +++ b/crates/sage/src/endpoints/data.rs @@ -19,12 +19,13 @@ use sage_api::{ GetMinterDidIds, GetMinterDidIdsResponse, GetNft, GetNftCollection, GetNftCollectionResponse, GetNftCollections, GetNftCollectionsResponse, GetNftData, GetNftDataResponse, GetNftIcon, GetNftIconResponse, GetNftResponse, GetNftThumbnail, GetNftThumbnailResponse, GetNfts, - GetNftsResponse, GetPendingTransactions, GetPendingTransactionsResponse, GetSpendableCoinCount, - GetSpendableCoinCountResponse, GetSyncStatus, GetSyncStatusResponse, GetToken, - GetTokenResponse, GetTransaction, GetTransactionResponse, GetTransactions, - GetTransactionsResponse, GetVersion, GetVersionResponse, NftCollectionRecord, NftData, - NftRecord, NftSortMode as ApiNftSortMode, PendingTransactionRecord, PerformDatabaseMaintenance, - PerformDatabaseMaintenanceResponse, TokenRecord, TransactionCoinRecord, TransactionRecord, + GetNftsResponse, GetPendingTransactions, GetPendingTransactionsResponse, GetProfile, + GetProfileResponse, GetSpendableCoinCount, GetSpendableCoinCountResponse, GetSyncStatus, + GetSyncStatusResponse, GetToken, GetTokenResponse, GetTransaction, GetTransactionResponse, + GetTransactions, GetTransactionsResponse, GetVersion, GetVersionResponse, NftCollectionRecord, + NftData, NftRecord, NftSortMode as ApiNftSortMode, PendingTransactionRecord, + PerformDatabaseMaintenance, PerformDatabaseMaintenanceResponse, TokenRecord, + TransactionCoinRecord, TransactionRecord, }; use sage_database::{ AssetFilter, CoinFilterMode, CoinSortMode, NftGroupSearch, NftRow, NftSortMode, Transaction, @@ -357,6 +358,27 @@ impl Sage { Ok(GetTokenResponse { token }) } + pub async fn get_profile(&self, req: GetProfile) -> Result { + let wallet = self.wallet()?; + + let Some(row) = wallet.db.did(req.launcher_id).await? else { + return Ok(GetProfileResponse { did: None }); + }; + + let did = DidRecord { + launcher_id: Address::new(row.asset.hash, "did:chia:".to_string()).encode()?, + name: row.asset.name, + visible: row.asset.is_visible, + coin_id: hex::encode(row.coin_row.coin.coin_id()), + address: Address::new(row.coin_row.p2_puzzle_hash, self.network().prefix()).encode()?, + amount: Amount::u64(row.coin_row.coin.amount), + recovery_hash: row.did_info.recovery_list_hash.map(hex::encode), + created_height: row.coin_row.created_height, + }; + + Ok(GetProfileResponse { did: Some(did) }) + } + pub async fn get_dids(&self, _req: GetDids) -> Result { let wallet = self.wallet()?; diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx new file mode 100644 index 00000000..f9cd7fe1 --- /dev/null +++ b/src/pages/Profile.tsx @@ -0,0 +1,392 @@ +import Container from '@/components/Container'; +import { CopyBox } from '@/components/CopyBox'; +import Header from '@/components/Header'; +import ProfileCard from '@/components/ProfileCard'; +import { Button } from '@/components/ui/button'; +import { useErrors } from '@/hooks/useErrors'; +import spacescanLogo from '@/images/spacescan-logo-192.png'; +import { isAudio, isImage, isJson, isText, nftUri } from '@/lib/nftUri'; +import { isValidUrl } from '@/lib/utils'; +import { t } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; +import { openUrl } from '@tauri-apps/plugin-opener'; +import { useEffect, useMemo, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import { commands, DidRecord, events, NetworkKind } from '../bindings'; + +export default function Profile() { + const { launcher_id: launcherId } = useParams(); + const navigate = useNavigate(); + const { addError } = useErrors(); + const [did, setDid] = useState(null); + + const updateNft = useMemo( + () => () => { + commands + .getProfile({ did: launcherId ?? '' }) + .then((data) => setDid(data.did)) + .catch(addError); + }, + [launcherId, addError], + ); + + useEffect(() => { + updateNft(); + + const unlisten = events.syncEvent.listen((event) => { + const type = event.payload.type; + if ( + type === 'coin_state' || + type === 'puzzle_batch_synced' || + type === 'nft_data' + ) { + updateNft(); + } + }); + + return () => { + unlisten.then((u) => u()); + }; + }, [updateNft]); + + useEffect(() => { + commands + .getNftData({ nft_id: launcherId ?? '' }) + .then((response) => setData(response.data)) + .catch(addError); + }, [launcherId, addError]); + + const metadata = useMemo(() => { + if (!nft || !data?.metadata_json) return {}; + try { + return JSON.parse(data.metadata_json) ?? {}; + } catch { + return {}; + } + }, [data?.metadata_json, nft]); + + const [network, setNetwork] = useState(null); + + useEffect(() => { + commands + .getNetwork({}) + .then((data) => setNetwork(data.kind)) + .catch(addError); + }, [addError]); + + return ( + <> +
+ +
+ {isImage(data?.mime_type ?? null) ? ( + NFT image + ) : isText(data?.mime_type ?? null) ? ( +
+
+                {data?.blob ? atob(data.blob) : ''}
+              
+
+ ) : isJson(data?.mime_type ?? null) ? ( +
+
+                {data?.blob
+                  ? JSON.stringify(JSON.parse(atob(data.blob)), null, 2)
+                  : ''}
+              
+
+ ) : isAudio(data?.mime_type ?? null) ? ( +
+
🎵
+
+ ) : ( +
+ +
+
+ {nft?.edition_total != null && nft?.edition_total > 1 && ( +
+
+ + Edition {nft.edition_number} of {nft.edition_total} + +
+
+ )} + + {metadata.description && ( +
+
+ Description +
+
+ {metadata.description} +
+
+ )} + + {nft?.collection_name && ( +
+
+ Collection Name +
+
+ nft?.collection_id && + navigate(`/nfts/collections/${nft.collection_id}/metadata`) + } + > + {nft.collection_name} +
+
+ )} + + {(metadata.attributes?.length ?? 0) > 0 && ( +
+
+ Attributes +
+
+ {metadata.attributes.map( + (attr: { trait_type: string; value: string }) => ( +
+
+ {attr.trait_type} +
+ {isValidUrl(attr.value) ? ( +
openUrl(attr.value)} + className='text-sm break-all text-blue-700 dark:text-blue-300 cursor-pointer hover:underline' + > + {attr.value} +
+ ) : ( +
{attr.value}
+ )} +
+ ), + )} +
+
+ )} + + {!!nft?.data_uris.length && ( +
+
+ Data URIs +
+ {nft.data_uris.map((uri) => ( +
openUrl(uri)} + > + {uri} +
+ ))} +
+ )} + + {(nft?.metadata_uris.length || null) && ( +
+
+ Metadata URIs +
+ {nft?.metadata_uris.map((uri) => ( +
openUrl(uri)} + > + {uri} +
+ ))} +
+ )} + + {(nft?.license_uris.length || null) && ( +
+
+ License URIs +
+ {nft?.license_uris.map((uri) => ( +
openUrl(uri)} + > + {uri} +
+ ))} +
+ )} + + {nft?.data_hash && ( +
+
+ Data Hash +
+
{nft.data_hash}
+
+ )} + + {nft?.metadata_hash && ( +
+
+ Metadata Hash +
+
{nft.metadata_hash}
+
+ )} + + {nft?.license_hash && ( +
+
+ License Hash +
+
{nft.license_hash}
+
+ )} +
+ +
+
+
+ Minter DID +
+ toast.success(t`Minter DID copied to clipboard`)} + /> + {nft?.minter_did && ( +
+ +
+ )} +
+ +
+
+ Owner DID +
+ toast.success(t`Owner DID copied to clipboard`)} + /> + {nft?.owner_did && ( +
+ +
+ )} +
+ +
+
+ Address +
+ toast.success(t`Address copied to clipboard`)} + /> +
+ +
+
+ Coin Id +
+ toast.success(t`Coin ID copied to clipboard`)} + /> +
+ +
+
+ Royalties {royaltyPercentage}% +
+ + toast.success(t`Royalty address copied to clipboard`) + } + /> +
+ +
+
+ External Links +
+ + + + +
+
+
+
+ + ); +} From 5ded5b4599cc7afbcfd0472938f7d2656d4449d3 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Sun, 3 Aug 2025 17:57:26 -0500 Subject: [PATCH 10/19] owned_did --- crates/sage-api/endpoints.json | 1 + crates/sage-database/src/tables/assets/did.rs | 4 +-- crates/sage/src/endpoints/data.rs | 2 +- src-tauri/src/lib.rs | 1 + src/bindings.ts | 5 ++++ src/components/ProfileCard.tsx | 1 + src/pages/Profile.tsx | 30 +++++-------------- 7 files changed, 18 insertions(+), 26 deletions(-) diff --git a/crates/sage-api/endpoints.json b/crates/sage-api/endpoints.json index 9c6a409e..f24dab79 100644 --- a/crates/sage-api/endpoints.json +++ b/crates/sage-api/endpoints.json @@ -24,6 +24,7 @@ "get_all_cats": true, "get_token": true, "get_dids": true, + "get_profile": true, "get_minter_did_ids": true, "get_pending_transactions": true, "get_transaction": true, diff --git a/crates/sage-database/src/tables/assets/did.rs b/crates/sage-database/src/tables/assets/did.rs index 990b5c70..7109285e 100644 --- a/crates/sage-database/src/tables/assets/did.rs +++ b/crates/sage-database/src/tables/assets/did.rs @@ -19,7 +19,7 @@ pub struct DidRow { impl Database { pub async fn owned_did(&self, launcher_id: String) -> Result> { - let hash = launcher_id.as_ref(); + let hash: &str = launcher_id.as_ref(); query!( " @@ -76,7 +76,7 @@ impl Database { }, }) }) - .transpose()? + .transpose() } pub async fn owned_dids(&self) -> Result> { diff --git a/crates/sage/src/endpoints/data.rs b/crates/sage/src/endpoints/data.rs index 284e7e11..f17cc28b 100644 --- a/crates/sage/src/endpoints/data.rs +++ b/crates/sage/src/endpoints/data.rs @@ -361,7 +361,7 @@ impl Sage { pub async fn get_profile(&self, req: GetProfile) -> Result { let wallet = self.wallet()?; - let Some(row) = wallet.db.did(req.launcher_id).await? else { + let Some(row) = wallet.db.owned_did(req.launcher_id.clone()).await? else { return Ok(GetProfileResponse { did: None }); }; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 83dde696..874f9efc 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -68,6 +68,7 @@ pub fn run() { commands::get_all_cats, commands::get_token, commands::get_dids, + commands::get_profile, commands::get_minter_did_ids, commands::get_nft_collections, commands::get_nft_collection, diff --git a/src/bindings.ts b/src/bindings.ts index d1afb996..53c7e6c6 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -140,6 +140,9 @@ async getToken(req: GetToken) : Promise { async getDids(req: GetDids) : Promise { return await TAURI_INVOKE("get_dids", { req }); }, +async getProfile(req: GetProfile) : Promise { + return await TAURI_INVOKE("get_profile", { req }); +}, async getMinterDidIds(req: GetMinterDidIds) : Promise { return await TAURI_INVOKE("get_minter_did_ids", { req }); }, @@ -447,6 +450,8 @@ export type GetPeers = Record export type GetPeersResponse = { peers: PeerRecord[] } export type GetPendingTransactions = Record export type GetPendingTransactionsResponse = { transactions: PendingTransactionRecord[] } +export type GetProfile = { launcher_id: string } +export type GetProfileResponse = { did: DidRecord | null } export type GetSecretKey = { fingerprint: number } export type GetSecretKeyResponse = { secrets: SecretKeyInfo | null } export type GetSpendableCoinCount = { asset_id: string | null } diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index 98845d87..8df58210 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -315,6 +315,7 @@ export function ProfileCard({ : mintGardenProfile.name}{' '} {isOwned && !mintGardenProfile.is_unknown && + didRecord.name && didRecord.name !== mintGardenProfile.name && ( {`(${didRecord.name})`} diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index f9cd7fe1..72b404ed 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -21,10 +21,10 @@ export default function Profile() { const { addError } = useErrors(); const [did, setDid] = useState(null); - const updateNft = useMemo( + const updateDid = useMemo( () => () => { commands - .getProfile({ did: launcherId ?? '' }) + .getProfile({ launcher_id: launcherId ?? '' }) .then((data) => setDid(data.did)) .catch(addError); }, @@ -32,39 +32,23 @@ export default function Profile() { ); useEffect(() => { - updateNft(); + updateDid(); const unlisten = events.syncEvent.listen((event) => { const type = event.payload.type; if ( type === 'coin_state' || type === 'puzzle_batch_synced' || - type === 'nft_data' + type === 'did_info' ) { - updateNft(); + updateDid(); } }); return () => { unlisten.then((u) => u()); }; - }, [updateNft]); - - useEffect(() => { - commands - .getNftData({ nft_id: launcherId ?? '' }) - .then((response) => setData(response.data)) - .catch(addError); - }, [launcherId, addError]); - - const metadata = useMemo(() => { - if (!nft || !data?.metadata_json) return {}; - try { - return JSON.parse(data.metadata_json) ?? {}; - } catch { - return {}; - } - }, [data?.metadata_json, nft]); + }, [updateDid]); const [network, setNetwork] = useState(null); @@ -77,7 +61,7 @@ export default function Profile() { return ( <> -
+
{isImage(data?.mime_type ?? null) ? ( From 460af74405fe50a50a3c4b44c444a4ed35274912 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Sun, 3 Aug 2025 18:28:11 -0500 Subject: [PATCH 11/19] navigate to profile page --- src/App.tsx | 4 +- src/components/DidDisplay.tsx | 152 +++++++++++++ src/components/DidInfo.tsx | 44 ++++ src/components/ProfileCard.tsx | 14 +- src/hooks/useDidData.ts | 52 +++++ src/lib/mintGardenService.ts | 1 - src/pages/CollectionMetaData.tsx | 31 +-- src/pages/Nft.tsx | 35 +-- src/pages/Profile.tsx | 378 ++----------------------------- 9 files changed, 289 insertions(+), 422 deletions(-) create mode 100644 src/components/DidDisplay.tsx create mode 100644 src/components/DidInfo.tsx create mode 100644 src/hooks/useDidData.ts diff --git a/src/App.tsx b/src/App.tsx index dcfa2086..86ce830d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -52,6 +52,7 @@ import { Transactions } from './pages/Transactions'; import { ViewOffer } from './pages/ViewOffer'; import { ViewSavedOffer } from './pages/ViewSavedOffer'; import Wallet from './pages/Wallet'; +import Profile from './pages/Profile'; const router = createHashRouter( createRoutesFromElements( @@ -77,10 +78,11 @@ const router = createHashRouter( } /> } /> } /> - + }> } /> } /> + } /> }> } /> diff --git a/src/components/DidDisplay.tsx b/src/components/DidDisplay.tsx new file mode 100644 index 00000000..537ac94e --- /dev/null +++ b/src/components/DidDisplay.tsx @@ -0,0 +1,152 @@ +import { CopyBox } from '@/components/CopyBox'; +import ProfileCard from '@/components/ProfileCard'; +import { Button } from '@/components/ui/button'; +import { useDidData } from '@/hooks/useDidData'; +import spacescanLogo from '@/images/spacescan-logo-192.png'; +import { t } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; +import { openUrl } from '@tauri-apps/plugin-opener'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { commands, NetworkKind } from '../bindings'; + +export interface DidDisplayProps { + launcherId: string; + title?: string; + showExternalLinks?: boolean; + className?: string; +} + +export function DidDisplay({ + launcherId, + title, + showExternalLinks = true, + className = '', +}: DidDisplayProps) { + const { did, isLoading } = useDidData({ launcherId }); + const [network, setNetwork] = useState(null); + + useEffect(() => { + commands + .getNetwork({}) + .then((data) => setNetwork(data.kind)) + .catch(() => { + // Network fetch failed, continue with default state + }); + }, []); + + if (isLoading) { + return ( +
+
+
+
+
+
+ ); + } + + if (!did) { + return ( +
+
+ DID not found +
+
+ ); + } + + return ( +
+ {title &&

{title}

} + +
+
+
+ DID Information +
+ +
+ +
+
+ Launcher ID +
+ toast.success(t`Launcher ID copied to clipboard`)} + /> +
+ + {did.address && ( +
+
+ Address +
+ toast.success(t`Address copied to clipboard`)} + /> +
+ )} + + {did.coin_id && did.coin_id !== '0' && ( +
+
+ Coin ID +
+ toast.success(t`Coin ID copied to clipboard`)} + /> +
+ )} + + {showExternalLinks && ( +
+
+ External Links +
+ + + + +
+ )} +
+
+ ); +} diff --git a/src/components/DidInfo.tsx b/src/components/DidInfo.tsx new file mode 100644 index 00000000..c43a3170 --- /dev/null +++ b/src/components/DidInfo.tsx @@ -0,0 +1,44 @@ +import { CopyBox } from '@/components/CopyBox'; +import ProfileCard from '@/components/ProfileCard'; +import { t } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; +import { toast } from 'react-toastify'; + +export interface DidInfoProps { + did?: string | null; + title: string; + className?: string; +} + +export function DidInfo({ did, title, className = '' }: DidInfoProps) { + if (!did) { + return ( +
+
+ {title} +
+ toast.success(t`DID copied to clipboard`)} + /> +
+ ); + } + + return ( +
+
+ {title} +
+ toast.success(t`DID copied to clipboard`)} + /> +
+ +
+
+ ); +} diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index 8df58210..12586d30 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -44,6 +44,7 @@ import { SendIcon, } from 'lucide-react'; import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { toast } from 'react-toastify'; import { AssetKind, @@ -78,6 +79,7 @@ export function ProfileCard({ }: ProfileCardProps) { const { addError } = useErrors(); const walletState = useWalletState(); + const navigate = useNavigate(); // this component can be used to display a DID record or a DID string // if it is a DID DidRecord, it will be treated as a local DID and will have @@ -262,6 +264,10 @@ export function ProfileCard({ } }; + const handleDidClick = () => { + navigate(`/dids/${didRecord.launcher_id}`); + }; + // Loading state if (isMintGardenLoading) { return ( @@ -431,7 +437,13 @@ export function ProfileCard({ ) : null} -
+
{didAsset.asset_id}
diff --git a/src/hooks/useDidData.ts b/src/hooks/useDidData.ts new file mode 100644 index 00000000..cd658662 --- /dev/null +++ b/src/hooks/useDidData.ts @@ -0,0 +1,52 @@ +import { useErrors } from '@/hooks/useErrors'; +import { commands, DidRecord, events } from '../bindings'; +import { useEffect, useMemo, useState } from 'react'; + +export interface UseDidDataParams { + launcherId: string; +} + +export function useDidData({ launcherId }: UseDidDataParams) { + const { addError } = useErrors(); + const [did, setDid] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const updateDid = useMemo( + () => () => { + if (!launcherId) return; + + setIsLoading(true); + commands + .getProfile({ launcher_id: launcherId }) + .then((data) => setDid(data.did)) + .catch(addError) + .finally(() => setIsLoading(false)); + }, + [launcherId, addError], + ); + + useEffect(() => { + updateDid(); + + const unlisten = events.syncEvent.listen((event) => { + const type = event.payload.type; + if ( + type === 'coin_state' || + type === 'puzzle_batch_synced' || + type === 'did_info' + ) { + updateDid(); + } + }); + + return () => { + unlisten.then((u) => u()); + }; + }, [updateDid]); + + return { + did, + isLoading, + updateDid, + }; +} diff --git a/src/lib/mintGardenService.ts b/src/lib/mintGardenService.ts index 5a7e64c0..4883d839 100644 --- a/src/lib/mintGardenService.ts +++ b/src/lib/mintGardenService.ts @@ -319,4 +319,3 @@ export async function getMintGardenProfile( // Export the service class and configuration interface for advanced usage export { MintGardenService }; export type { MintGardenServiceConfig }; - diff --git a/src/pages/CollectionMetaData.tsx b/src/pages/CollectionMetaData.tsx index 362af636..f374f0a3 100644 --- a/src/pages/CollectionMetaData.tsx +++ b/src/pages/CollectionMetaData.tsx @@ -2,12 +2,11 @@ import { commands, NetworkKind, NftCollectionRecord } from '@/bindings'; import Container from '@/components/Container'; import { CopyBox } from '@/components/CopyBox'; import Header from '@/components/Header'; -import ProfileCard, { MintGardenProfile } from '@/components/ProfileCard'; +import { DidInfo } from '@/components/DidInfo'; import { Button } from '@/components/ui/button'; import { CustomError } from '@/contexts/ErrorContext'; import { useErrors } from '@/hooks/useErrors'; import spacescanLogo from '@/images/spacescan-logo-192.png'; -import { getMintGardenProfile } from '@/lib/marketplaces'; import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { openUrl } from '@tauri-apps/plugin-opener'; @@ -36,9 +35,6 @@ export default function CollectionMetaData() { useState(null); const [loading, setLoading] = useState(true); const [network, setNetwork] = useState(null); - const [minterProfile, setMinterProfile] = useState( - null, - ); useEffect(() => { async function fetchData() { @@ -90,15 +86,6 @@ export default function CollectionMetaData() { fetchData(); }, [collection_id, addError]); - useEffect(() => { - if (!collection?.did_id) { - setMinterProfile(null); - return; - } - - getMintGardenProfile(collection.did_id).then(setMinterProfile); - }, [collection?.did_id]); - useEffect(() => { commands .getNetwork({}) @@ -368,21 +355,7 @@ export default function CollectionMetaData() { />
-
-
- Minter DID -
- toast.success(t`Minter DID copied to clipboard`)} - /> - {minterProfile && ( -
- -
- )} -
+
diff --git a/src/pages/Nft.tsx b/src/pages/Nft.tsx index e5237a61..49510ddc 100644 --- a/src/pages/Nft.tsx +++ b/src/pages/Nft.tsx @@ -1,7 +1,7 @@ import Container from '@/components/Container'; import { CopyBox } from '@/components/CopyBox'; import Header from '@/components/Header'; -import ProfileCard from '@/components/ProfileCard'; +import { DidInfo } from '@/components/DidInfo'; import { Button } from '@/components/ui/button'; import { useErrors } from '@/hooks/useErrors'; import spacescanLogo from '@/images/spacescan-logo-192.png'; @@ -280,37 +280,8 @@ export default function Nft() {
-
-
- Minter DID -
- toast.success(t`Minter DID copied to clipboard`)} - /> - {nft?.minter_did && ( -
- -
- )} -
- -
-
- Owner DID -
- toast.success(t`Owner DID copied to clipboard`)} - /> - {nft?.owner_did && ( -
- -
- )} -
+ +
diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 72b404ed..9d6a8f20 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -1,375 +1,37 @@ import Container from '@/components/Container'; -import { CopyBox } from '@/components/CopyBox'; import Header from '@/components/Header'; -import ProfileCard from '@/components/ProfileCard'; -import { Button } from '@/components/ui/button'; -import { useErrors } from '@/hooks/useErrors'; -import spacescanLogo from '@/images/spacescan-logo-192.png'; -import { isAudio, isImage, isJson, isText, nftUri } from '@/lib/nftUri'; -import { isValidUrl } from '@/lib/utils'; +import { DidDisplay } from '@/components/DidDisplay'; +import { useParams } from 'react-router-dom'; import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; -import { openUrl } from '@tauri-apps/plugin-opener'; -import { useEffect, useMemo, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; -import { commands, DidRecord, events, NetworkKind } from '../bindings'; export default function Profile() { const { launcher_id: launcherId } = useParams(); - const navigate = useNavigate(); - const { addError } = useErrors(); - const [did, setDid] = useState(null); - const updateDid = useMemo( - () => () => { - commands - .getProfile({ launcher_id: launcherId ?? '' }) - .then((data) => setDid(data.did)) - .catch(addError); - }, - [launcherId, addError], - ); - - useEffect(() => { - updateDid(); - - const unlisten = events.syncEvent.listen((event) => { - const type = event.payload.type; - if ( - type === 'coin_state' || - type === 'puzzle_batch_synced' || - type === 'did_info' - ) { - updateDid(); - } - }); - - return () => { - unlisten.then((u) => u()); - }; - }, [updateDid]); - - const [network, setNetwork] = useState(null); - - useEffect(() => { - commands - .getNetwork({}) - .then((data) => setNetwork(data.kind)) - .catch(addError); - }, [addError]); + if (!launcherId) { + return ( + <> +
+ +
+ Invalid DID ID +
+
+ + ); + } return ( <> -
+
-
- {isImage(data?.mime_type ?? null) ? ( - NFT image - ) : isText(data?.mime_type ?? null) ? ( -
-
-                {data?.blob ? atob(data.blob) : ''}
-              
-
- ) : isJson(data?.mime_type ?? null) ? ( -
-
-                {data?.blob
-                  ? JSON.stringify(JSON.parse(atob(data.blob)), null, 2)
-                  : ''}
-              
-
- ) : isAudio(data?.mime_type ?? null) ? ( -
-
🎵
-
- ) : ( -