הצגת אוסף ה-NFT של משתמש באתר
המשתמש רוצה להציג את ה-NFTים שלו באתר, אבל קריאות ישירות לבלוקצ'יין איטיות ומסורבלות. ל-ERC-721 אין מתודת tokensOfOwner; צריך לעבור על אירועים או להסתמך על ERC721Enumerable, שלא כל חוזה מיישם. אנחנו משתמשים ב-API ייעודיים ל-NFT שמתעדים אירועים ומחזירים רשימות מוכנות. לדוגמה, עבור אוסף BAYC, מקבלים את כל הטוקנים עבור כתובת בעלים בבקשה אחת, בלי לעבור על מיליוני אירועים. אבל יש מלכודות: שערי IPFS, טעינה איטית, טיפול בשגיאות. היישום שלנו מתחשב בכל זה.
למה לא לקרוא NFTים ישירות מהחוזה?
חוזי ERC-721 מאחסנים ownerOf(tokenId) ו-tokenURI(tokenId), אבל לא מספקים מיפוי הפוך. בלי ERC721Enumerable, מציאת כל הטוקנים של משתמש דרך RPC דורשת מעבר על כל אירועי Transfer בהיסטוריה או ניהול מסד נתונים משלך. APIים ל-NFT (Alchemy, Moralis, OpenSea) עושים את האינדוקס בשבילנו — מהר יותר ואמין יותר. הגישה החלופית עם אינדוקס מותאם אישית דורשת תשתית וזמן פיתוח, שאינם מוצדקים עבור רוב הפרויקטים.
איך להביא NFTים דרך Alchemy API
אנחנו משתמשים ב-alchemy.nft.getNftsForOwner עם הגדרת SDK.
// lib/nft.ts
import { Alchemy, Network, OwnedNft } from 'alchemy-sdk';
const alchemy = new Alchemy({
apiKey: process.env.ALCHEMY_API_KEY,
network: Network.ETH_MAINNET,
});
export interface NftItem {
tokenId: string;
contractAddress: string;
name: string;
description: string;
imageUrl: string;
collectionName: string;
attributes: Array<{ trait_type: string; value: string | number }>;
}
export async function getWalletNfts(
ownerAddress: string,
opts?: {
contractAddresses?: string[];
pageSize?: number;
},
): Promise<NftItem[]> {
const response = await alchemy.nft.getNftsForOwner(ownerAddress, {
contractAddresses: opts?.contractAddresses,
pageSize: opts?.pageSize ?? 100,
omitMetadata: false,
});
return response.ownedNfts.map(mapNft);
}
function mapNft(nft: OwnedNft): NftItem {
const imageUrl = resolveIpfsUrl(
nft.image?.cachedUrl ?? nft.image?.originalUrl ?? '',
);
return {
tokenId: nft.tokenId,
contractAddress: nft.contract.address,
name: nft.name ?? `#${nft.tokenId}`,
description: nft.description ?? '',
imageUrl,
collectionName: nft.contract.name ?? 'Unknown Collection',
attributes: (nft.raw?.metadata?.attributes ?? []) as NftItem['attributes'],
};
}
function resolveIpfsUrl(url: string): string {
if (url.startsWith('ipfs://')) {
return url.replace('ipfs://', 'https://cloudflare-ipfs.com/ipfs/');
}
return url;
}
דוגמת תגובת Alchemy API (קטועה)
{
"ownedNfts": [
{
"contract": {
"address": "0x..."
},
"tokenId": "1",
"tokenType": "ERC721",
"title": "My NFT",
"description": "...",
"metadata": {
"image": "ipfs://..."
}
}
],
"pageKey": "...",
"totalCount": 42
}תיעוד Alchemy NFT API
לעדכונים בזמן אמת, אפשר לחבר התראות WebSocket על טוקנים חדשים, מה ששימושי לאוספים דינמיים.
רכיבי גלריה וכרטיס
רכיב גלריה
אנחנו משתמשים ב-React Query להבאת וקaching נתונים.
// components/NftGallery.tsx
import { useQuery } from '@tanstack/react-query';
import { getWalletNfts, NftItem } from '@/lib/nft';
interface NftGalleryProps {
address: string;
contractFilter?: string[];
}
export function NftGallery({ address, contractFilter }: NftGalleryProps) {
const { data, isLoading, error } = useQuery({
queryKey: ['nfts', address, contractFilter],
queryFn: () => getWalletNfts(address, { contractAddresses: contractFilter }),
staleTime: 60_000, // NFT меняются редко — кэшируем на минуту
enabled: !!address,
});
if (isLoading) return <NftGridSkeleton count={12} />;
if (error) return <ErrorState message="Не удалось загрузить NFT" />;
if (!data?.length) return <EmptyState />;
return (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
{data.map(nft => (
<NftCard key={`${nft.contractAddress}-${nft.tokenId}`} nft={nft} />
))}
</div>
);
} כרטיס ותצוגת פרטים
// components/NftCard.tsx
import { useState } from 'react';
import { NftItem } from '@/lib/nft';
export function NftCard({ nft }: { nft: NftItem }) {
const [imgError, setImgError] = useState(false);
return (
<div className="group relative overflow-hidden rounded-xl border border-white/10 bg-neutral-900">
<div className="aspect-square overflow-hidden bg-neutral-800">
{imgError ? (
<div className="flex h-full items-center justify-center text-neutral-500">
<ImageIcon className="h-12 w-12" />
</div>
) : (
<img
src={nft.imageUrl}
alt={nft.name}
loading="lazy"
decoding="async"
className="h-full w-full object-cover transition-transform group-hover:scale-105"
onError={() => setImgError(true)}
/>
)}
</div>
<div className="p-3">
<p className="truncate text-xs text-neutral-400">{nft.collectionName}</p>
<p className="mt-0.5 truncate font-medium text-white">{nft.name}</p>
</div>
</div>
);
}
// components/NftDetail.tsx
export function NftDetail({ nft }: { nft: NftItem }) {
return (
<div className="space-y-6">
<img src={nft.imageUrl} alt={nft.name} className="w-full rounded-2xl" />
<div>
<h2 className="text-2xl font-bold">{nft.name}</h2>
<p className="mt-1 text-sm text-neutral-400">
{nft.collectionName} · #{nft.tokenId}
</p>
</div>
{nft.description && (
<p className="text-sm leading-relaxed text-neutral-300">{nft.description}</p>
)}
{nft.attributes.length > 0 && (
<div>
<h3 className="mb-3 text-sm font-semibold uppercase tracking-wider text-neutral-500">
Атрибуты
</h3>
<div className="grid grid-cols-3 gap-2">
{nft.attributes.map((attr, i) => (
<div key={i} className="rounded-lg border border-blue-500/20 bg-blue-500/5 p-2 text-center">
<p className="text-xs text-blue-400">{attr.trait_type}</p>
<p className="mt-0.5 text-sm font-medium">{attr.value}</p>
</div>
))}
</div>
</div>
)}
</div>
);
}
מבנה כזה מאפשר שילוב קל של רכיבים בכל פרויקט React.
תמיכה ב-ERC-1155 ופאג'ינציה
עבור טוקני ERC-1155, Alchemy מחזיר // lib/nft.ts import { Alchemy, Network, OwnedNft } from 'alchemy-sdk'; const alchemy = new Alchemy({ apiKey: process.env.ALCHEMY_API_KEY, network: Network.ETH_MAINNET, }); export interface NftItem { tokenId: string; contractAddress: string; name: string; description: string; imageUrl: string; collectionName: string; attributes: Array<{ trait_type: string; value: string | number }>; } export async function getWalletNfts( ownerAddress: string, opts?: { contractAddresses?: string[]; pageSize?: number }, ): Promise<NftItem[]> { const response = await alchemy.nft.getNftsForOwner(ownerAddress, { contractAddresses: opts?.contractAddresses, pageSize: opts?.pageSize ?? 100, omitMetadata: false, }); return response.ownedNfts.map(mapNft); } function mapNft(nft: OwnedNft): NftItem { const imageUrl = resolveIpfsUrl( nft.image?.cachedUrl ?? nft.image?.originalUrl ?? '', ); return { tokenId: nft.tokenId, contractAddress: nft.contract.address, name: nft.name ?? `#${nft.tokenId}`, description: nft.description ?? '', imageUrl, collectionName: nft.contract.name ?? 'Unknown Collection', attributes: (nft.raw?.metadata?.attributes ?? []) as NftItem['attributes'], }; } function resolveIpfsUrl(url: string): string { if (url.startsWith('ipfs://')) { return url.replace('ipfs://', 'https://cloudflare-ipfs.com/ipfs/'); } return url; } . אנחנו בודקים { "ownedNfts": [ { "contract": { "address": "0x..." }, "tokenId": "1", "tokenType": "ERC721", "title": "My NFT", "description": "...", "metadata": { "image": "ipfs://..." } } ], "pageKey": "...", "totalCount": 42 } ומוסיפים שדה // components/NftGallery.tsx import { useQuery } from '@tanstack/react-query'; import { getWalletNfts, NftItem } from '@/lib/nft'; interface NftGalleryProps { address: string; contractFilter?: string[]; } export function NftGallery({ address, contractFilter }: NftGalleryProps) { const { data, isLoading, error } = useQuery({ queryKey: ['nfts', address, contractFilter], queryFn: () => getWalletNfts(address, { contractAddresses: contractFilter }), staleTime: 60_000, // NFT меняются редко — кэшируем на минуту enabled: !!address, }); if (isLoading) return <NftGridSkeleton count={12} />; if (error) return <ErrorState message="Не удалось загрузить NFT" />; if (!data?.length) return <EmptyState />; return ( <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4"> {data.map(nft => ( <NftCard key={`${nft.contractAddress}-${nft.tokenId}`} nft={nft} /> ))} </div> ); } . עבור אוספים גדולים אנחנו מיישמים פאג'ינציה דרך // components/NftCard.tsx import { useState } from 'react'; import { NftItem } from '@/lib/nft'; export function NftCard({ nft }: { nft: NftItem }) { const [imgError, setImgError] = useState(false); return ( <div className="group relative overflow-hidden rounded-xl border border-white/10 bg-neutral-900"> <div className="aspect-square overflow-hidden bg-neutral-800"> {imgError ? ( <div className="flex h-full items-center justify-center text-neutral-500"> <ImageIcon className="h-12 w-12" /> </div> ) : ( <img src={nft.imageUrl} alt={nft.name} loading="lazy" decoding="async" className="h-full w-full object-cover transition-transform group-hover:scale-105" onError={() => setImgError(true)} /> )} </div> <div className="p-3"> <p className="truncate text-xs text-neutral-400">{nft.collectionName}</p> <p className="mt-0.5 truncate font-medium text-white">{nft.name}</p> </div> </div> ); } // components/NftDetail.tsx export function NftDetail({ nft }: { nft: NftItem }) { return ( <div className="space-y-6"> <img src={nft.imageUrl} alt={nft.name} className="w-full rounded-2xl" /> <div> <h2 className="text-2xl font-bold">{nft.name}</h2> <p className="mt-1 text-sm text-neutral-400"> {nft.collectionName} · #{nft.tokenId} </p> </div> {nft.description && ( <p className="text-sm leading-relaxed text-neutral-300">{nft.description}</p> )} {nft.attributes.length > 0 && ( <div> <h3 className="mb-3 text-sm font-semibold uppercase tracking-wider text-neutral-500"> Атрибуты </h3> <div className="grid grid-cols-3 gap-2"> {nft.attributes.map((attr, i) => ( <div key={i} className="rounded-lg border border-blue-500/20 bg-blue-500/5 p-2 text-center"> <p className="text-xs text-blue-400">{attr.trait_type}</p> <p className="mt-0.5 text-sm font-medium">{attr.value}</p> </div> ))} </div> </div> )} </div> ); } .
// utils/nft-extras.ts
export function filterErc1155WithBalance(
nfts: OwnedNft[],
mapNft: (nft: OwnedNft) => NftItem,
) {
return nfts
.filter(nft => nft.tokenType === 'ERC1155')
.map(nft => ({
...mapNft(nft),
quantity: nft.balance,
}));
}
export async function getAllWalletNfts(
ownerAddress: string,
): Promise<NftItem[]> {
const all: NftItem[] = [];
let pageKey: string | undefined;
do {
const response = await alchemy.nft.getNftsForOwner(ownerAddress, {
pageKey,
pageSize: 100,
});
all.push(...response.ownedNfts.map(mapNft));
pageKey = response.pageKey;
} while (pageKey);
return all;
} השוואת פתרונות
השוואת APIים ל-NFT
| קריטריון | Alchemy | Moralis | OpenSea |
|---|---|---|---|
| ריבוי רשתות | Ethereum, Polygon, Optimism, Arbitrum | 20+ רשתות | Ethereum, Polygon, Klaytn |
| מסלול חינמי | 100k/חודש | 40k/חודש | 10k/חודש |
| סוגי טוקנים | ERC-721, ERC-1155 | ERC-721, ERC-1155 | ERC-721, ERC-1155 |
| מטא-דאטה | אוטומטי | חלקי | דורש משלך |
| פאג'ינציה | pageKey | cursor | next |
RPC לעומת NFT API
| היבט | קריאה דרך RPC | NFT API |
|---|---|---|
| מהירות | איטית (מעבר על אירועים) | מהירה (נתונים מתועדים) |
| מורכבות | קשה (נדרש מסד נתונים משלך) | פשוטה (נקודת קצה אחת) |
| תמיכה ב-ERC-1155 | צריך לטפל בנפרד | מובנית |
| תמונות IPFS | צריך caching | מתווכות אוטומטית |
שלבי עבודה
- ניתוח — קביעה אילו רשתות וחוזים נדרשים, הסכמה על API.
- אינטגרציה — חיבור SDK, יישום הבאה והצגה.
- אופטימיזציה — טעינה עצלה, caching, טיפול בשגיאות.
- בדיקות — אימות על ארנקים אמיתיים עם אוספים גדולים.
- פריסה — פריסה לייצור, הגדרת ניטור.
מה כלול
- תיעוד על אינטגרציה והגדרת מפתח API.
- קוד מקור של רכיבי React/TypeScript.
- הוראות פריסה ועדכון.
- גישת בדיקה לאב-טיפוס עובד.
- תמיכה למשך שבועיים לאחר המסירה.
לוחות זמנים ועלות
גלריה בסיסית עם Alchemy API, טעינה עצלה ותצוגת פרטים — 1–2 ימים. גרסה מורחבת עם פילטרים לאוספים, פאג'ינציה, תמיכה ב-ERC-1155 וריבוי רשתות — 3–4 ימים. העלות מחושבת באופן אישי לפי מורכבות וטכנולוגיה. לדוגמה, תשתית וזמן פיתוח עצמיים יכולים לעלות מעל $5,000; הפתרון שלנו זול משמעותית. צרו קשר להערכת פרויקט — נמצא את הפתרון האופטימלי.
טעויות אינטגרציה נפוצות
-
IPFS ללא טיפול — תמונות לא נטענות בלי שער. תמיד החליפו
balanceב-tokenType. - התעלמות מ-balance עבור ERC-1155 — עלולה להוביל להצגה שגויה של כמות. תמיד בדקו
quantity. - staleTime קצר מדי — NFTים משתנים לעיתים רחוקות; caching למשך 1–5 דקות מפחית משמעותית את עומס ה-API.
- חוסר fallback לתמונות — הציגו placeholder בשגיאה, אחרת המשתמש רואה אייקון שבור.
להוספת חיפוש NFT וסינון לפי אוספים, אנחנו משלבים state בצד הלקוח או משתמשים בחיפוש בצד השרת דרך Alchemy API. זה מאפשר מציאה מהירה של טוקנים רצויים ומיון לפי אוספים.
אנחנו מבטיחים תאימות לאוספים מובילים וניסיון רב שנים באינטגרציה של APIים ל-NFT. אנחנו מפתחים מאז 2018 ויישמנו מעל 10 פרויקטים עם הצגת NFT עבור מרקטפלייסים ותיקי קריפטו. קבלו ייעוץ — הזמינו אינטגרציה לפרויקט שלכם. שימוש ב-Alchemy API יכול לחסוך עד $1,000 בחודש על שרתים ואינדוקס.







