@b3dotfun/sdk 0.1.0-alpha.3 → 0.1.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/global-account/react/components/AccountAssets/AccountAssets.js +7 -3
- package/dist/cjs/global-account/react/components/ManageAccount/NFTContent.js +2 -2
- package/dist/cjs/global-account/react/hooks/index.d.ts +1 -0
- package/dist/cjs/global-account/react/hooks/index.js +4 -2
- package/dist/cjs/global-account/react/hooks/useSimBalance.js +10 -15
- package/dist/cjs/global-account/react/hooks/useSimCollectibles.d.ts +45 -0
- package/dist/cjs/global-account/react/hooks/useSimCollectibles.js +190 -0
- package/dist/cjs/global-account/react/utils/simdune.d.ts +7 -0
- package/dist/cjs/global-account/react/utils/simdune.js +21 -0
- package/dist/esm/global-account/react/components/AccountAssets/AccountAssets.js +7 -3
- package/dist/esm/global-account/react/components/ManageAccount/NFTContent.js +3 -3
- package/dist/esm/global-account/react/hooks/index.d.ts +1 -0
- package/dist/esm/global-account/react/hooks/index.js +1 -0
- package/dist/esm/global-account/react/hooks/useSimBalance.js +10 -15
- package/dist/esm/global-account/react/hooks/useSimCollectibles.d.ts +45 -0
- package/dist/esm/global-account/react/hooks/useSimCollectibles.js +187 -0
- package/dist/esm/global-account/react/utils/simdune.d.ts +7 -0
- package/dist/esm/global-account/react/utils/simdune.js +17 -0
- package/dist/types/global-account/react/hooks/index.d.ts +1 -0
- package/dist/types/global-account/react/hooks/useSimCollectibles.d.ts +45 -0
- package/dist/types/global-account/react/utils/simdune.d.ts +7 -0
- package/package.json +1 -1
- package/src/global-account/react/components/AccountAssets/AccountAssets.tsx +25 -13
- package/src/global-account/react/components/ManageAccount/NFTContent.tsx +4 -4
- package/src/global-account/react/hooks/index.ts +1 -0
- package/src/global-account/react/hooks/useSimBalance.ts +10 -16
- package/src/global-account/react/hooks/useSimCollectibles.ts +238 -0
- package/src/global-account/react/utils/simdune.ts +20 -0
|
@@ -21,6 +21,10 @@ function AccountAssets({ nfts, isLoading }) {
|
|
|
21
21
|
}, {});
|
|
22
22
|
const collections = Object.values(groupedNFTs || {});
|
|
23
23
|
const [expandedCollections, setExpandedCollections] = (0, react_1.useState)(() => new Set(collections.map(c => c.collection_id)));
|
|
24
|
+
const [failedImages, setFailedImages] = (0, react_1.useState)(() => new Set());
|
|
25
|
+
const handleImageError = (imageId) => {
|
|
26
|
+
setFailedImages(prev => new Set(prev).add(imageId));
|
|
27
|
+
};
|
|
24
28
|
if (isLoading) {
|
|
25
29
|
return ((0, jsx_runtime_1.jsx)("div", { className: "flex flex-col gap-3", children: [...Array(2)].map((_, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "animate-pulse", children: [(0, jsx_runtime_1.jsx)("div", { className: "bg-b3-react-muted mb-3 h-6 w-48 rounded" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-3", children: [(0, jsx_runtime_1.jsx)("div", { className: "bg-b3-react-muted h-[98px] w-[98px] shrink-0 rounded-lg" }), (0, jsx_runtime_1.jsx)("div", { className: "bg-b3-react-muted h-[98px] w-[98px] shrink-0 rounded-lg" })] })] }, i))) }));
|
|
26
30
|
}
|
|
@@ -39,11 +43,11 @@ function AccountAssets({ nfts, isLoading }) {
|
|
|
39
43
|
return next;
|
|
40
44
|
});
|
|
41
45
|
};
|
|
42
|
-
return ((0, jsx_runtime_1.jsx)("div", { className: "flex flex-col gap-3
|
|
46
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "flex flex-col gap-3", children: collections.map(collection => {
|
|
43
47
|
const isExpanded = expandedCollections.has(collection.collection_id);
|
|
44
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-3", children: [(0, jsx_runtime_1.jsxs)("button", { onClick: () => toggleCollection(collection.collection_id), className: "flex w-full items-center justify-between", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1", children: [collection.collection_image && ((0, jsx_runtime_1.jsx)("img", { src: collection.collection_image, alt: collection.collection_name, className: "h-5 w-5 shrink-0 rounded object-cover" })), (0, jsx_runtime_1.jsxs)("p", { className: "font-neue-montreal-medium text-[14px] text-[#3f3f46]", children: [collection.collection_name, " (", collection.nfts.length, ")"] })] }), (0, jsx_runtime_1.jsx)("svg", { className: `h-[18px] w-[18px] shrink-0 transition-transform ${isExpanded ? "rotate-180" : ""}`, viewBox: "0 0 18 18", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: (0, jsx_runtime_1.jsx)("path", { d: "M4.5 6.75L9 11.25L13.5 6.75", stroke: "#51525C", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) })] }), isExpanded && ((0, jsx_runtime_1.jsx)("div", { className: "flex gap-3 overflow-x-auto", children: collection.nfts.map(nft => ((0, jsx_runtime_1.jsx)("div", { className: "relative h-[98px] w-[98px] shrink-0 overflow-hidden rounded-lg", children: (0, jsx_runtime_1.jsx)("img", { src: nft.previews?.image_medium_url ||
|
|
48
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-3", children: [(0, jsx_runtime_1.jsxs)("button", { onClick: () => toggleCollection(collection.collection_id), className: "flex w-full items-center justify-between", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1", children: [collection.collection_image && !failedImages.has(`collection-${collection.collection_id}`) && ((0, jsx_runtime_1.jsx)("img", { src: collection.collection_image, alt: collection.collection_name, className: "h-5 w-5 shrink-0 rounded object-cover", onError: () => handleImageError(`collection-${collection.collection_id}`) })), (0, jsx_runtime_1.jsxs)("p", { className: "font-neue-montreal-medium text-[14px] text-[#3f3f46]", children: [collection.collection_name, " (", collection.nfts.length, ")"] })] }), (0, jsx_runtime_1.jsx)("svg", { className: `h-[18px] w-[18px] shrink-0 transition-transform ${isExpanded ? "rotate-180" : ""}`, viewBox: "0 0 18 18", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: (0, jsx_runtime_1.jsx)("path", { d: "M4.5 6.75L9 11.25L13.5 6.75", stroke: "#51525C", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) })] }), isExpanded && ((0, jsx_runtime_1.jsx)("div", { className: "flex gap-3 overflow-x-auto", children: collection.nfts.map(nft => ((0, jsx_runtime_1.jsx)("div", { className: "bg-b3-react-muted relative h-[98px] w-[98px] shrink-0 overflow-hidden rounded-lg", children: !failedImages.has(nft.nft_id) && ((0, jsx_runtime_1.jsx)("img", { src: nft.previews?.image_medium_url ||
|
|
45
49
|
nft.extra_metadata?.image_original_url ||
|
|
46
50
|
nft.collection?.image_url ||
|
|
47
|
-
"", alt: nft.name || "NFT", className: "h-full w-full object-cover" }) }, nft.nft_id))) }))] }, collection.collection_id));
|
|
51
|
+
"", alt: nft.name || "NFT", className: "h-full w-full object-cover", onError: () => handleImageError(nft.nft_id) })) }, nft.nft_id))) }))] }, collection.collection_id));
|
|
48
52
|
}) }));
|
|
49
53
|
}
|
|
@@ -9,7 +9,7 @@ const NFTContent = () => {
|
|
|
9
9
|
const activeWallet = (0, react_1.useActiveWallet)();
|
|
10
10
|
const activeAccount = activeWallet?.getAccount();
|
|
11
11
|
const activeAddress = activeAccount?.address;
|
|
12
|
-
const { data: nfts, isLoading } = (0, hooks_1.
|
|
13
|
-
return ((0, jsx_runtime_1.jsx)("div", { style: { minHeight: "100px" }, children: nfts
|
|
12
|
+
const { data: nfts, isLoading } = (0, hooks_1.useSimCollectibles)(activeAddress, [1, 8453], { filterSpam: true });
|
|
13
|
+
return ((0, jsx_runtime_1.jsx)("div", { style: { minHeight: "100px" }, children: nfts ? ((0, jsx_runtime_1.jsx)(__1.AccountAssets, { nfts: nfts, isLoading: isLoading })) : ((0, jsx_runtime_1.jsx)("div", { className: "py-12 text-center text-gray-500", children: "No NFTs found" })) }));
|
|
14
14
|
};
|
|
15
15
|
exports.default = NFTContent;
|
|
@@ -31,6 +31,7 @@ export { useRemoveSessionKey } from "./useRemoveSessionKey";
|
|
|
31
31
|
export { useRouter } from "./useRouter";
|
|
32
32
|
export { useSearchParamsSSR } from "./useSearchParamsSSR";
|
|
33
33
|
export { useSimBalance, useSimSvmBalance, useSimTokenBalance } from "./useSimBalance";
|
|
34
|
+
export { useSimCollectibles } from "./useSimCollectibles";
|
|
34
35
|
export { useSiwe } from "./useSiwe";
|
|
35
36
|
export { useTokenBalance } from "./useTokenBalance";
|
|
36
37
|
export { useTokenBalanceDirect } from "./useTokenBalanceDirect";
|
|
@@ -14,8 +14,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.
|
|
18
|
-
exports.useUser = void 0;
|
|
17
|
+
exports.useUnifiedChainSwitchAndExecute = exports.useTurnkeyAuth = exports.useTokensFromAddress = exports.useTokenPriceWithFallback = exports.useTokenPrice = exports.useTokenFromUrl = exports.useTokenData = exports.useTokenBalancesByChain = exports.useTokenBalanceDirect = exports.useTokenBalance = exports.useSiwe = exports.useSimCollectibles = exports.useSimTokenBalance = exports.useSimSvmBalance = exports.useSimBalance = exports.useSearchParamsSSR = exports.useRouter = exports.useRemoveSessionKey = exports.useQueryBSMNT = exports.useQueryB3 = exports.useProfileSettings = exports.useProfilePreference = exports.useProfile = exports.useDisplayName = exports.useOneBalance = exports.useNotifications = exports.useNativeBalanceFromRPC = exports.useNativeBalance = exports.useMediaQuery = exports.useIsomorphicLayoutEffect = exports.useIsMobile = exports.useHasMounted = exports.useHandleConnectWithPrivy = exports.useGlobalAccount = exports.useGetGeo = exports.useGetAllTWSigners = exports.useFirstEOA = exports.useExchangeRate = exports.useConnect = exports.useClient = exports.useChainSwitchWithAction = exports.useB3EnsName = exports.useB3BalanceFromAddresses = exports.useAuthentication = exports.useAuth = exports.useAnalytics = exports.useAddTWSessionKey = exports.useAccountWallet = exports.useAccountAssets = exports.createWagmiConfig = void 0;
|
|
18
|
+
exports.useUser = exports.useURLParams = void 0;
|
|
19
19
|
var createWagmiConfig_1 = require("../utils/createWagmiConfig");
|
|
20
20
|
Object.defineProperty(exports, "createWagmiConfig", { enumerable: true, get: function () { return createWagmiConfig_1.createWagmiConfig; } });
|
|
21
21
|
var useAccountAssets_1 = require("./useAccountAssets");
|
|
@@ -87,6 +87,8 @@ var useSimBalance_1 = require("./useSimBalance");
|
|
|
87
87
|
Object.defineProperty(exports, "useSimBalance", { enumerable: true, get: function () { return useSimBalance_1.useSimBalance; } });
|
|
88
88
|
Object.defineProperty(exports, "useSimSvmBalance", { enumerable: true, get: function () { return useSimBalance_1.useSimSvmBalance; } });
|
|
89
89
|
Object.defineProperty(exports, "useSimTokenBalance", { enumerable: true, get: function () { return useSimBalance_1.useSimTokenBalance; } });
|
|
90
|
+
var useSimCollectibles_1 = require("./useSimCollectibles");
|
|
91
|
+
Object.defineProperty(exports, "useSimCollectibles", { enumerable: true, get: function () { return useSimCollectibles_1.useSimCollectibles; } });
|
|
90
92
|
var useSiwe_1 = require("./useSiwe");
|
|
91
93
|
Object.defineProperty(exports, "useSiwe", { enumerable: true, get: function () { return useSiwe_1.useSiwe; } });
|
|
92
94
|
var useTokenBalance_1 = require("./useTokenBalance");
|
|
@@ -4,14 +4,16 @@ exports.useSimBalance = useSimBalance;
|
|
|
4
4
|
exports.useSimSvmBalance = useSimSvmBalance;
|
|
5
5
|
exports.useSimTokenBalance = useSimTokenBalance;
|
|
6
6
|
const react_query_1 = require("@tanstack/react-query");
|
|
7
|
+
const simdune_1 = require("../utils/simdune");
|
|
7
8
|
async function fetchSimBalance(address, chainIdsParam) {
|
|
8
9
|
if (!address)
|
|
9
10
|
throw new Error("Address is required");
|
|
10
11
|
const chainIds = chainIdsParam.length === 0 ? "mainnet" : chainIdsParam.join(",");
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const queryParams = new URLSearchParams();
|
|
13
|
+
queryParams.append("metadata", "logo");
|
|
14
|
+
queryParams.append("chain_ids", chainIds);
|
|
15
|
+
queryParams.append("exclude_spam_tokens", "true");
|
|
16
|
+
const url = (0, simdune_1.buildSimduneUrl)(`/v1/evm/balances/${address}`, queryParams);
|
|
15
17
|
const response = await fetch(url);
|
|
16
18
|
if (!response.ok) {
|
|
17
19
|
throw new Error(`Failed to fetch balance: ${response.statusText}`);
|
|
@@ -26,10 +28,9 @@ async function fetchSimTokenBalance(walletAddress, tokenAddress, chainId) {
|
|
|
26
28
|
throw new Error("Token address is required");
|
|
27
29
|
if (!chainId)
|
|
28
30
|
throw new Error("Chain ID is required");
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
31
|
+
const queryParams = new URLSearchParams();
|
|
32
|
+
queryParams.append("chain_ids", chainId.toString());
|
|
33
|
+
const url = (0, simdune_1.buildSimduneUrl)(`/v1/evm/balances/${walletAddress}/token/${tokenAddress}`, queryParams);
|
|
33
34
|
const response = await fetch(url);
|
|
34
35
|
if (!response.ok) {
|
|
35
36
|
throw new Error(`Failed to fetch token balance: ${response.statusText}`);
|
|
@@ -47,13 +48,7 @@ async function fetchSimSvmBalance(address, chains, limit) {
|
|
|
47
48
|
if (limit) {
|
|
48
49
|
queryParams.append("limit", limit.toString());
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
-
if (queryParams.toString()) {
|
|
52
|
-
url += `?${queryParams.toString()}`;
|
|
53
|
-
}
|
|
54
|
-
if (process.env.NEXT_PUBLIC_DEVMODE_SHARED_SECRET) {
|
|
55
|
-
url += `${queryParams.toString() ? "&" : "?"}localkey=${process.env.NEXT_PUBLIC_DEVMODE_SHARED_SECRET}`;
|
|
56
|
-
}
|
|
51
|
+
const url = (0, simdune_1.buildSimduneUrl)(`/beta/svm/balances/${address}`, queryParams.toString() ? queryParams : undefined);
|
|
57
52
|
const response = await fetch(url);
|
|
58
53
|
if (!response.ok) {
|
|
59
54
|
throw new Error(`Failed to fetch SVM balance: ${response.statusText}`);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { SimpleHashNFTResponse } from "../../../global-account/types/simplehash.types";
|
|
2
|
+
export interface SimCollectibleMetadata {
|
|
3
|
+
uri: string;
|
|
4
|
+
attributes?: Array<{
|
|
5
|
+
trait_type: string;
|
|
6
|
+
value: string;
|
|
7
|
+
display_type?: string | null;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export interface SimCollectibleEntry {
|
|
11
|
+
contract_address: string;
|
|
12
|
+
token_standard: "ERC721" | "ERC1155";
|
|
13
|
+
token_id: string;
|
|
14
|
+
chain: string;
|
|
15
|
+
chain_id: number;
|
|
16
|
+
name?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
symbol?: string;
|
|
19
|
+
image_url?: string;
|
|
20
|
+
last_sale_price?: string;
|
|
21
|
+
metadata?: SimCollectibleMetadata;
|
|
22
|
+
is_spam?: boolean;
|
|
23
|
+
spam_score?: number;
|
|
24
|
+
explanations?: string[];
|
|
25
|
+
balance?: string;
|
|
26
|
+
last_acquired?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface SimCollectiblesResponse {
|
|
29
|
+
address: string;
|
|
30
|
+
entries: SimCollectibleEntry[];
|
|
31
|
+
next_offset?: string;
|
|
32
|
+
request_time: string;
|
|
33
|
+
response_time: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Hook to fetch NFT collectibles from Simdune API.
|
|
37
|
+
* Returns data in SimpleHash format for compatibility with AccountAssets component.
|
|
38
|
+
* @param address - Wallet address to fetch collectibles for
|
|
39
|
+
* @param chainIdsParam - Optional array of chain IDs to filter by
|
|
40
|
+
* @param options - Optional parameters (limit, filterSpam)
|
|
41
|
+
*/
|
|
42
|
+
export declare function useSimCollectibles(address?: string, chainIdsParam?: number[], options?: {
|
|
43
|
+
limit?: number;
|
|
44
|
+
filterSpam?: boolean;
|
|
45
|
+
}): import("@tanstack/react-query").UseQueryResult<SimpleHashNFTResponse, Error>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useSimCollectibles = useSimCollectibles;
|
|
4
|
+
const react_query_1 = require("@tanstack/react-query");
|
|
5
|
+
const simdune_1 = require("../utils/simdune");
|
|
6
|
+
/**
|
|
7
|
+
* Safely parse a balance string to a number, capping at MAX_SAFE_INTEGER
|
|
8
|
+
* to prevent overflow issues with large ERC1155 balances.
|
|
9
|
+
*/
|
|
10
|
+
function safeParseBalance(balance) {
|
|
11
|
+
if (!balance)
|
|
12
|
+
return 1;
|
|
13
|
+
const parsed = parseInt(balance, 10);
|
|
14
|
+
if (Number.isNaN(parsed))
|
|
15
|
+
return 1;
|
|
16
|
+
return Math.min(parsed, Number.MAX_SAFE_INTEGER);
|
|
17
|
+
}
|
|
18
|
+
async function fetchSimCollectibles(address, chainIdsParam, options) {
|
|
19
|
+
if (!address)
|
|
20
|
+
throw new Error("Address is required");
|
|
21
|
+
const queryParams = new URLSearchParams();
|
|
22
|
+
if (chainIdsParam && chainIdsParam.length > 0) {
|
|
23
|
+
queryParams.append("chain_ids", chainIdsParam.join(","));
|
|
24
|
+
}
|
|
25
|
+
if (options?.limit) {
|
|
26
|
+
queryParams.append("limit", options.limit.toString());
|
|
27
|
+
}
|
|
28
|
+
if (options?.filterSpam !== undefined) {
|
|
29
|
+
queryParams.append("filter_spam", options.filterSpam.toString());
|
|
30
|
+
}
|
|
31
|
+
const url = (0, simdune_1.buildSimduneUrl)(`/v1/evm/collectibles/${address}`, queryParams);
|
|
32
|
+
const response = await fetch(url);
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`Failed to fetch collectibles: ${response.statusText}`);
|
|
35
|
+
}
|
|
36
|
+
const data = await response.json();
|
|
37
|
+
return data;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Transforms Simdune collectibles response to SimpleHash NFT format
|
|
41
|
+
* for compatibility with existing AccountAssets component
|
|
42
|
+
*/
|
|
43
|
+
function transformToSimpleHashFormat(data) {
|
|
44
|
+
const nfts = data.entries.map(entry => ({
|
|
45
|
+
nft_id: `${entry.chain}.${entry.contract_address}.${entry.token_id}`,
|
|
46
|
+
chain: entry.chain,
|
|
47
|
+
contract_address: entry.contract_address,
|
|
48
|
+
token_id: entry.token_id,
|
|
49
|
+
name: entry.name || "",
|
|
50
|
+
description: entry.description || "",
|
|
51
|
+
previews: {
|
|
52
|
+
image_small_url: entry.image_url || "",
|
|
53
|
+
image_medium_url: entry.image_url || "",
|
|
54
|
+
image_large_url: entry.image_url || "",
|
|
55
|
+
image_opengraph_url: entry.image_url || "",
|
|
56
|
+
blurhash: "",
|
|
57
|
+
predominant_color: "",
|
|
58
|
+
},
|
|
59
|
+
image_url: entry.image_url || "",
|
|
60
|
+
image_properties: {
|
|
61
|
+
width: 0,
|
|
62
|
+
height: 0,
|
|
63
|
+
size: 0,
|
|
64
|
+
mime_type: "",
|
|
65
|
+
exif_orientation: null,
|
|
66
|
+
},
|
|
67
|
+
video_url: null,
|
|
68
|
+
video_properties: null,
|
|
69
|
+
audio_url: null,
|
|
70
|
+
audio_properties: null,
|
|
71
|
+
model_url: null,
|
|
72
|
+
model_properties: null,
|
|
73
|
+
other_url: null,
|
|
74
|
+
other_properties: null,
|
|
75
|
+
background_color: null,
|
|
76
|
+
external_url: null,
|
|
77
|
+
created_date: "",
|
|
78
|
+
status: "minted",
|
|
79
|
+
token_count: 1,
|
|
80
|
+
owner_count: 1,
|
|
81
|
+
owners: [
|
|
82
|
+
{
|
|
83
|
+
owner_address: data.address,
|
|
84
|
+
quantity: safeParseBalance(entry.balance),
|
|
85
|
+
quantity_string: entry.balance || "1",
|
|
86
|
+
first_acquired_date: entry.last_acquired || "",
|
|
87
|
+
last_acquired_date: entry.last_acquired || "",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
contract: {
|
|
91
|
+
type: entry.token_standard,
|
|
92
|
+
name: entry.name || "",
|
|
93
|
+
symbol: entry.symbol || null,
|
|
94
|
+
deployed_by: "",
|
|
95
|
+
deployed_via_contract: "",
|
|
96
|
+
owned_by: "",
|
|
97
|
+
has_multiple_collections: false,
|
|
98
|
+
},
|
|
99
|
+
collection: {
|
|
100
|
+
collection_id: entry.contract_address,
|
|
101
|
+
name: entry.symbol || "Unknown Collection",
|
|
102
|
+
description: null,
|
|
103
|
+
image_url: entry.image_url || "",
|
|
104
|
+
image_properties: {
|
|
105
|
+
width: 0,
|
|
106
|
+
height: 0,
|
|
107
|
+
mime_type: "",
|
|
108
|
+
},
|
|
109
|
+
banner_image_url: null,
|
|
110
|
+
category: null,
|
|
111
|
+
is_nsfw: null,
|
|
112
|
+
external_url: null,
|
|
113
|
+
twitter_username: null,
|
|
114
|
+
discord_url: null,
|
|
115
|
+
instagram_username: null,
|
|
116
|
+
medium_username: null,
|
|
117
|
+
telegram_url: null,
|
|
118
|
+
marketplace_pages: [],
|
|
119
|
+
metaplex_mint: null,
|
|
120
|
+
metaplex_candy_machine: null,
|
|
121
|
+
metaplex_first_verified_creator: null,
|
|
122
|
+
floor_prices: [],
|
|
123
|
+
top_bids: [],
|
|
124
|
+
distinct_owner_count: 0,
|
|
125
|
+
distinct_nft_count: 0,
|
|
126
|
+
total_quantity: 0,
|
|
127
|
+
chains: [entry.chain],
|
|
128
|
+
top_contracts: [entry.contract_address],
|
|
129
|
+
collection_royalties: [],
|
|
130
|
+
},
|
|
131
|
+
last_sale: entry.last_sale_price
|
|
132
|
+
? {
|
|
133
|
+
price: entry.last_sale_price,
|
|
134
|
+
}
|
|
135
|
+
: null,
|
|
136
|
+
primary_sale: null,
|
|
137
|
+
first_created: {
|
|
138
|
+
minted_to: data.address,
|
|
139
|
+
quantity: 1,
|
|
140
|
+
quantity_string: "1",
|
|
141
|
+
timestamp: "",
|
|
142
|
+
block_number: 0,
|
|
143
|
+
transaction: "",
|
|
144
|
+
transaction_initiator: "",
|
|
145
|
+
},
|
|
146
|
+
rarity: {
|
|
147
|
+
rank: null,
|
|
148
|
+
score: null,
|
|
149
|
+
unique_attributes: null,
|
|
150
|
+
},
|
|
151
|
+
royalty: [],
|
|
152
|
+
extra_metadata: {
|
|
153
|
+
attributes: (entry.metadata?.attributes || []).map(attr => ({
|
|
154
|
+
trait_type: attr.trait_type,
|
|
155
|
+
value: attr.value,
|
|
156
|
+
display_type: attr.display_type ?? null,
|
|
157
|
+
})),
|
|
158
|
+
image_original_url: entry.image_url || "",
|
|
159
|
+
animation_original_url: null,
|
|
160
|
+
metadata_original_url: entry.metadata?.uri || "",
|
|
161
|
+
},
|
|
162
|
+
}));
|
|
163
|
+
return {
|
|
164
|
+
next_cursor: data.next_offset || null,
|
|
165
|
+
next: null,
|
|
166
|
+
previous: null,
|
|
167
|
+
nfts,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Hook to fetch NFT collectibles from Simdune API.
|
|
172
|
+
* Returns data in SimpleHash format for compatibility with AccountAssets component.
|
|
173
|
+
* @param address - Wallet address to fetch collectibles for
|
|
174
|
+
* @param chainIdsParam - Optional array of chain IDs to filter by
|
|
175
|
+
* @param options - Optional parameters (limit, filterSpam)
|
|
176
|
+
*/
|
|
177
|
+
function useSimCollectibles(address, chainIdsParam, options) {
|
|
178
|
+
return (0, react_query_1.useQuery)({
|
|
179
|
+
queryKey: ["simCollectibles", address, chainIdsParam, options],
|
|
180
|
+
queryFn: async () => {
|
|
181
|
+
if (!address)
|
|
182
|
+
throw new Error("Address is required");
|
|
183
|
+
const data = await fetchSimCollectibles(address, chainIdsParam, options);
|
|
184
|
+
return transformToSimpleHashFormat(data);
|
|
185
|
+
},
|
|
186
|
+
enabled: Boolean(address),
|
|
187
|
+
staleTime: 30 * 1000,
|
|
188
|
+
gcTime: 5 * 60 * 1000,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const SIMDUNE_API_HOST = "https://simdune-api.sean-430.workers.dev";
|
|
2
|
+
/**
|
|
3
|
+
* Builds a Simdune API URL with the proxy wrapper and optional dev mode key.
|
|
4
|
+
* @param endpoint - The Simdune API endpoint (e.g., "/v1/evm/balances/0x...")
|
|
5
|
+
* @param queryParams - Optional URLSearchParams to append
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildSimduneUrl(endpoint: string, queryParams?: URLSearchParams): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SIMDUNE_API_HOST = void 0;
|
|
4
|
+
exports.buildSimduneUrl = buildSimduneUrl;
|
|
5
|
+
exports.SIMDUNE_API_HOST = "https://simdune-api.sean-430.workers.dev";
|
|
6
|
+
/**
|
|
7
|
+
* Builds a Simdune API URL with the proxy wrapper and optional dev mode key.
|
|
8
|
+
* @param endpoint - The Simdune API endpoint (e.g., "/v1/evm/balances/0x...")
|
|
9
|
+
* @param queryParams - Optional URLSearchParams to append
|
|
10
|
+
*/
|
|
11
|
+
function buildSimduneUrl(endpoint, queryParams) {
|
|
12
|
+
const baseUrl = `${exports.SIMDUNE_API_HOST}/?url=https://api.sim.dune.com${endpoint}`;
|
|
13
|
+
let url = baseUrl;
|
|
14
|
+
if (queryParams && queryParams.toString()) {
|
|
15
|
+
url += `?${queryParams.toString()}`;
|
|
16
|
+
}
|
|
17
|
+
if (process.env.NEXT_PUBLIC_DEVMODE_SHARED_SECRET) {
|
|
18
|
+
url += `${queryParams?.toString() ? "&" : "?"}localkey=${process.env.NEXT_PUBLIC_DEVMODE_SHARED_SECRET}`;
|
|
19
|
+
}
|
|
20
|
+
return url;
|
|
21
|
+
}
|
|
@@ -18,6 +18,10 @@ export function AccountAssets({ nfts, isLoading }) {
|
|
|
18
18
|
}, {});
|
|
19
19
|
const collections = Object.values(groupedNFTs || {});
|
|
20
20
|
const [expandedCollections, setExpandedCollections] = useState(() => new Set(collections.map(c => c.collection_id)));
|
|
21
|
+
const [failedImages, setFailedImages] = useState(() => new Set());
|
|
22
|
+
const handleImageError = (imageId) => {
|
|
23
|
+
setFailedImages(prev => new Set(prev).add(imageId));
|
|
24
|
+
};
|
|
21
25
|
if (isLoading) {
|
|
22
26
|
return (_jsx("div", { className: "flex flex-col gap-3", children: [...Array(2)].map((_, i) => (_jsxs("div", { className: "animate-pulse", children: [_jsx("div", { className: "bg-b3-react-muted mb-3 h-6 w-48 rounded" }), _jsxs("div", { className: "flex gap-3", children: [_jsx("div", { className: "bg-b3-react-muted h-[98px] w-[98px] shrink-0 rounded-lg" }), _jsx("div", { className: "bg-b3-react-muted h-[98px] w-[98px] shrink-0 rounded-lg" })] })] }, i))) }));
|
|
23
27
|
}
|
|
@@ -36,11 +40,11 @@ export function AccountAssets({ nfts, isLoading }) {
|
|
|
36
40
|
return next;
|
|
37
41
|
});
|
|
38
42
|
};
|
|
39
|
-
return (_jsx("div", { className: "flex flex-col gap-3
|
|
43
|
+
return (_jsx("div", { className: "flex flex-col gap-3", children: collections.map(collection => {
|
|
40
44
|
const isExpanded = expandedCollections.has(collection.collection_id);
|
|
41
|
-
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("button", { onClick: () => toggleCollection(collection.collection_id), className: "flex w-full items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-1", children: [collection.collection_image && (_jsx("img", { src: collection.collection_image, alt: collection.collection_name, className: "h-5 w-5 shrink-0 rounded object-cover" })), _jsxs("p", { className: "font-neue-montreal-medium text-[14px] text-[#3f3f46]", children: [collection.collection_name, " (", collection.nfts.length, ")"] })] }), _jsx("svg", { className: `h-[18px] w-[18px] shrink-0 transition-transform ${isExpanded ? "rotate-180" : ""}`, viewBox: "0 0 18 18", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M4.5 6.75L9 11.25L13.5 6.75", stroke: "#51525C", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) })] }), isExpanded && (_jsx("div", { className: "flex gap-3 overflow-x-auto", children: collection.nfts.map(nft => (_jsx("div", { className: "relative h-[98px] w-[98px] shrink-0 overflow-hidden rounded-lg", children: _jsx("img", { src: nft.previews?.image_medium_url ||
|
|
45
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("button", { onClick: () => toggleCollection(collection.collection_id), className: "flex w-full items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-1", children: [collection.collection_image && !failedImages.has(`collection-${collection.collection_id}`) && (_jsx("img", { src: collection.collection_image, alt: collection.collection_name, className: "h-5 w-5 shrink-0 rounded object-cover", onError: () => handleImageError(`collection-${collection.collection_id}`) })), _jsxs("p", { className: "font-neue-montreal-medium text-[14px] text-[#3f3f46]", children: [collection.collection_name, " (", collection.nfts.length, ")"] })] }), _jsx("svg", { className: `h-[18px] w-[18px] shrink-0 transition-transform ${isExpanded ? "rotate-180" : ""}`, viewBox: "0 0 18 18", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M4.5 6.75L9 11.25L13.5 6.75", stroke: "#51525C", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) })] }), isExpanded && (_jsx("div", { className: "flex gap-3 overflow-x-auto", children: collection.nfts.map(nft => (_jsx("div", { className: "bg-b3-react-muted relative h-[98px] w-[98px] shrink-0 overflow-hidden rounded-lg", children: !failedImages.has(nft.nft_id) && (_jsx("img", { src: nft.previews?.image_medium_url ||
|
|
42
46
|
nft.extra_metadata?.image_original_url ||
|
|
43
47
|
nft.collection?.image_url ||
|
|
44
|
-
"", alt: nft.name || "NFT", className: "h-full w-full object-cover" }) }, nft.nft_id))) }))] }, collection.collection_id));
|
|
48
|
+
"", alt: nft.name || "NFT", className: "h-full w-full object-cover", onError: () => handleImageError(nft.nft_id) })) }, nft.nft_id))) }))] }, collection.collection_id));
|
|
45
49
|
}) }));
|
|
46
50
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useActiveWallet } from "thirdweb/react";
|
|
3
3
|
import { AccountAssets } from "..";
|
|
4
|
-
import {
|
|
4
|
+
import { useSimCollectibles } from "../../hooks/index.js";
|
|
5
5
|
const NFTContent = () => {
|
|
6
6
|
// Get active wallet state
|
|
7
7
|
const activeWallet = useActiveWallet();
|
|
8
8
|
const activeAccount = activeWallet?.getAccount();
|
|
9
9
|
const activeAddress = activeAccount?.address;
|
|
10
|
-
const { data: nfts, isLoading } =
|
|
11
|
-
return (_jsx("div", { style: { minHeight: "100px" }, children: nfts
|
|
10
|
+
const { data: nfts, isLoading } = useSimCollectibles(activeAddress, [1, 8453], { filterSpam: true });
|
|
11
|
+
return (_jsx("div", { style: { minHeight: "100px" }, children: nfts ? (_jsx(AccountAssets, { nfts: nfts, isLoading: isLoading })) : (_jsx("div", { className: "py-12 text-center text-gray-500", children: "No NFTs found" })) }));
|
|
12
12
|
};
|
|
13
13
|
export default NFTContent;
|
|
@@ -31,6 +31,7 @@ export { useRemoveSessionKey } from "./useRemoveSessionKey";
|
|
|
31
31
|
export { useRouter } from "./useRouter";
|
|
32
32
|
export { useSearchParamsSSR } from "./useSearchParamsSSR";
|
|
33
33
|
export { useSimBalance, useSimSvmBalance, useSimTokenBalance } from "./useSimBalance";
|
|
34
|
+
export { useSimCollectibles } from "./useSimCollectibles";
|
|
34
35
|
export { useSiwe } from "./useSiwe";
|
|
35
36
|
export { useTokenBalance } from "./useTokenBalance";
|
|
36
37
|
export { useTokenBalanceDirect } from "./useTokenBalanceDirect";
|
|
@@ -31,6 +31,7 @@ export { useRemoveSessionKey } from "./useRemoveSessionKey.js";
|
|
|
31
31
|
export { useRouter } from "./useRouter.js";
|
|
32
32
|
export { useSearchParamsSSR } from "./useSearchParamsSSR.js";
|
|
33
33
|
export { useSimBalance, useSimSvmBalance, useSimTokenBalance } from "./useSimBalance.js";
|
|
34
|
+
export { useSimCollectibles } from "./useSimCollectibles.js";
|
|
34
35
|
export { useSiwe } from "./useSiwe.js";
|
|
35
36
|
export { useTokenBalance } from "./useTokenBalance.js";
|
|
36
37
|
export { useTokenBalanceDirect } from "./useTokenBalanceDirect.js";
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { buildSimduneUrl } from "../utils/simdune.js";
|
|
2
3
|
async function fetchSimBalance(address, chainIdsParam) {
|
|
3
4
|
if (!address)
|
|
4
5
|
throw new Error("Address is required");
|
|
5
6
|
const chainIds = chainIdsParam.length === 0 ? "mainnet" : chainIdsParam.join(",");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const queryParams = new URLSearchParams();
|
|
8
|
+
queryParams.append("metadata", "logo");
|
|
9
|
+
queryParams.append("chain_ids", chainIds);
|
|
10
|
+
queryParams.append("exclude_spam_tokens", "true");
|
|
11
|
+
const url = buildSimduneUrl(`/v1/evm/balances/${address}`, queryParams);
|
|
10
12
|
const response = await fetch(url);
|
|
11
13
|
if (!response.ok) {
|
|
12
14
|
throw new Error(`Failed to fetch balance: ${response.statusText}`);
|
|
@@ -21,10 +23,9 @@ async function fetchSimTokenBalance(walletAddress, tokenAddress, chainId) {
|
|
|
21
23
|
throw new Error("Token address is required");
|
|
22
24
|
if (!chainId)
|
|
23
25
|
throw new Error("Chain ID is required");
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
26
|
+
const queryParams = new URLSearchParams();
|
|
27
|
+
queryParams.append("chain_ids", chainId.toString());
|
|
28
|
+
const url = buildSimduneUrl(`/v1/evm/balances/${walletAddress}/token/${tokenAddress}`, queryParams);
|
|
28
29
|
const response = await fetch(url);
|
|
29
30
|
if (!response.ok) {
|
|
30
31
|
throw new Error(`Failed to fetch token balance: ${response.statusText}`);
|
|
@@ -42,13 +43,7 @@ async function fetchSimSvmBalance(address, chains, limit) {
|
|
|
42
43
|
if (limit) {
|
|
43
44
|
queryParams.append("limit", limit.toString());
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
-
if (queryParams.toString()) {
|
|
47
|
-
url += `?${queryParams.toString()}`;
|
|
48
|
-
}
|
|
49
|
-
if (process.env.NEXT_PUBLIC_DEVMODE_SHARED_SECRET) {
|
|
50
|
-
url += `${queryParams.toString() ? "&" : "?"}localkey=${process.env.NEXT_PUBLIC_DEVMODE_SHARED_SECRET}`;
|
|
51
|
-
}
|
|
46
|
+
const url = buildSimduneUrl(`/beta/svm/balances/${address}`, queryParams.toString() ? queryParams : undefined);
|
|
52
47
|
const response = await fetch(url);
|
|
53
48
|
if (!response.ok) {
|
|
54
49
|
throw new Error(`Failed to fetch SVM balance: ${response.statusText}`);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { SimpleHashNFTResponse } from "../../../global-account/types/simplehash.types";
|
|
2
|
+
export interface SimCollectibleMetadata {
|
|
3
|
+
uri: string;
|
|
4
|
+
attributes?: Array<{
|
|
5
|
+
trait_type: string;
|
|
6
|
+
value: string;
|
|
7
|
+
display_type?: string | null;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export interface SimCollectibleEntry {
|
|
11
|
+
contract_address: string;
|
|
12
|
+
token_standard: "ERC721" | "ERC1155";
|
|
13
|
+
token_id: string;
|
|
14
|
+
chain: string;
|
|
15
|
+
chain_id: number;
|
|
16
|
+
name?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
symbol?: string;
|
|
19
|
+
image_url?: string;
|
|
20
|
+
last_sale_price?: string;
|
|
21
|
+
metadata?: SimCollectibleMetadata;
|
|
22
|
+
is_spam?: boolean;
|
|
23
|
+
spam_score?: number;
|
|
24
|
+
explanations?: string[];
|
|
25
|
+
balance?: string;
|
|
26
|
+
last_acquired?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface SimCollectiblesResponse {
|
|
29
|
+
address: string;
|
|
30
|
+
entries: SimCollectibleEntry[];
|
|
31
|
+
next_offset?: string;
|
|
32
|
+
request_time: string;
|
|
33
|
+
response_time: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Hook to fetch NFT collectibles from Simdune API.
|
|
37
|
+
* Returns data in SimpleHash format for compatibility with AccountAssets component.
|
|
38
|
+
* @param address - Wallet address to fetch collectibles for
|
|
39
|
+
* @param chainIdsParam - Optional array of chain IDs to filter by
|
|
40
|
+
* @param options - Optional parameters (limit, filterSpam)
|
|
41
|
+
*/
|
|
42
|
+
export declare function useSimCollectibles(address?: string, chainIdsParam?: number[], options?: {
|
|
43
|
+
limit?: number;
|
|
44
|
+
filterSpam?: boolean;
|
|
45
|
+
}): import("@tanstack/react-query").UseQueryResult<SimpleHashNFTResponse, Error>;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { buildSimduneUrl } from "../utils/simdune.js";
|
|
3
|
+
/**
|
|
4
|
+
* Safely parse a balance string to a number, capping at MAX_SAFE_INTEGER
|
|
5
|
+
* to prevent overflow issues with large ERC1155 balances.
|
|
6
|
+
*/
|
|
7
|
+
function safeParseBalance(balance) {
|
|
8
|
+
if (!balance)
|
|
9
|
+
return 1;
|
|
10
|
+
const parsed = parseInt(balance, 10);
|
|
11
|
+
if (Number.isNaN(parsed))
|
|
12
|
+
return 1;
|
|
13
|
+
return Math.min(parsed, Number.MAX_SAFE_INTEGER);
|
|
14
|
+
}
|
|
15
|
+
async function fetchSimCollectibles(address, chainIdsParam, options) {
|
|
16
|
+
if (!address)
|
|
17
|
+
throw new Error("Address is required");
|
|
18
|
+
const queryParams = new URLSearchParams();
|
|
19
|
+
if (chainIdsParam && chainIdsParam.length > 0) {
|
|
20
|
+
queryParams.append("chain_ids", chainIdsParam.join(","));
|
|
21
|
+
}
|
|
22
|
+
if (options?.limit) {
|
|
23
|
+
queryParams.append("limit", options.limit.toString());
|
|
24
|
+
}
|
|
25
|
+
if (options?.filterSpam !== undefined) {
|
|
26
|
+
queryParams.append("filter_spam", options.filterSpam.toString());
|
|
27
|
+
}
|
|
28
|
+
const url = buildSimduneUrl(`/v1/evm/collectibles/${address}`, queryParams);
|
|
29
|
+
const response = await fetch(url);
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`Failed to fetch collectibles: ${response.statusText}`);
|
|
32
|
+
}
|
|
33
|
+
const data = await response.json();
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Transforms Simdune collectibles response to SimpleHash NFT format
|
|
38
|
+
* for compatibility with existing AccountAssets component
|
|
39
|
+
*/
|
|
40
|
+
function transformToSimpleHashFormat(data) {
|
|
41
|
+
const nfts = data.entries.map(entry => ({
|
|
42
|
+
nft_id: `${entry.chain}.${entry.contract_address}.${entry.token_id}`,
|
|
43
|
+
chain: entry.chain,
|
|
44
|
+
contract_address: entry.contract_address,
|
|
45
|
+
token_id: entry.token_id,
|
|
46
|
+
name: entry.name || "",
|
|
47
|
+
description: entry.description || "",
|
|
48
|
+
previews: {
|
|
49
|
+
image_small_url: entry.image_url || "",
|
|
50
|
+
image_medium_url: entry.image_url || "",
|
|
51
|
+
image_large_url: entry.image_url || "",
|
|
52
|
+
image_opengraph_url: entry.image_url || "",
|
|
53
|
+
blurhash: "",
|
|
54
|
+
predominant_color: "",
|
|
55
|
+
},
|
|
56
|
+
image_url: entry.image_url || "",
|
|
57
|
+
image_properties: {
|
|
58
|
+
width: 0,
|
|
59
|
+
height: 0,
|
|
60
|
+
size: 0,
|
|
61
|
+
mime_type: "",
|
|
62
|
+
exif_orientation: null,
|
|
63
|
+
},
|
|
64
|
+
video_url: null,
|
|
65
|
+
video_properties: null,
|
|
66
|
+
audio_url: null,
|
|
67
|
+
audio_properties: null,
|
|
68
|
+
model_url: null,
|
|
69
|
+
model_properties: null,
|
|
70
|
+
other_url: null,
|
|
71
|
+
other_properties: null,
|
|
72
|
+
background_color: null,
|
|
73
|
+
external_url: null,
|
|
74
|
+
created_date: "",
|
|
75
|
+
status: "minted",
|
|
76
|
+
token_count: 1,
|
|
77
|
+
owner_count: 1,
|
|
78
|
+
owners: [
|
|
79
|
+
{
|
|
80
|
+
owner_address: data.address,
|
|
81
|
+
quantity: safeParseBalance(entry.balance),
|
|
82
|
+
quantity_string: entry.balance || "1",
|
|
83
|
+
first_acquired_date: entry.last_acquired || "",
|
|
84
|
+
last_acquired_date: entry.last_acquired || "",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
contract: {
|
|
88
|
+
type: entry.token_standard,
|
|
89
|
+
name: entry.name || "",
|
|
90
|
+
symbol: entry.symbol || null,
|
|
91
|
+
deployed_by: "",
|
|
92
|
+
deployed_via_contract: "",
|
|
93
|
+
owned_by: "",
|
|
94
|
+
has_multiple_collections: false,
|
|
95
|
+
},
|
|
96
|
+
collection: {
|
|
97
|
+
collection_id: entry.contract_address,
|
|
98
|
+
name: entry.symbol || "Unknown Collection",
|
|
99
|
+
description: null,
|
|
100
|
+
image_url: entry.image_url || "",
|
|
101
|
+
image_properties: {
|
|
102
|
+
width: 0,
|
|
103
|
+
height: 0,
|
|
104
|
+
mime_type: "",
|
|
105
|
+
},
|
|
106
|
+
banner_image_url: null,
|
|
107
|
+
category: null,
|
|
108
|
+
is_nsfw: null,
|
|
109
|
+
external_url: null,
|
|
110
|
+
twitter_username: null,
|
|
111
|
+
discord_url: null,
|
|
112
|
+
instagram_username: null,
|
|
113
|
+
medium_username: null,
|
|
114
|
+
telegram_url: null,
|
|
115
|
+
marketplace_pages: [],
|
|
116
|
+
metaplex_mint: null,
|
|
117
|
+
metaplex_candy_machine: null,
|
|
118
|
+
metaplex_first_verified_creator: null,
|
|
119
|
+
floor_prices: [],
|
|
120
|
+
top_bids: [],
|
|
121
|
+
distinct_owner_count: 0,
|
|
122
|
+
distinct_nft_count: 0,
|
|
123
|
+
total_quantity: 0,
|
|
124
|
+
chains: [entry.chain],
|
|
125
|
+
top_contracts: [entry.contract_address],
|
|
126
|
+
collection_royalties: [],
|
|
127
|
+
},
|
|
128
|
+
last_sale: entry.last_sale_price
|
|
129
|
+
? {
|
|
130
|
+
price: entry.last_sale_price,
|
|
131
|
+
}
|
|
132
|
+
: null,
|
|
133
|
+
primary_sale: null,
|
|
134
|
+
first_created: {
|
|
135
|
+
minted_to: data.address,
|
|
136
|
+
quantity: 1,
|
|
137
|
+
quantity_string: "1",
|
|
138
|
+
timestamp: "",
|
|
139
|
+
block_number: 0,
|
|
140
|
+
transaction: "",
|
|
141
|
+
transaction_initiator: "",
|
|
142
|
+
},
|
|
143
|
+
rarity: {
|
|
144
|
+
rank: null,
|
|
145
|
+
score: null,
|
|
146
|
+
unique_attributes: null,
|
|
147
|
+
},
|
|
148
|
+
royalty: [],
|
|
149
|
+
extra_metadata: {
|
|
150
|
+
attributes: (entry.metadata?.attributes || []).map(attr => ({
|
|
151
|
+
trait_type: attr.trait_type,
|
|
152
|
+
value: attr.value,
|
|
153
|
+
display_type: attr.display_type ?? null,
|
|
154
|
+
})),
|
|
155
|
+
image_original_url: entry.image_url || "",
|
|
156
|
+
animation_original_url: null,
|
|
157
|
+
metadata_original_url: entry.metadata?.uri || "",
|
|
158
|
+
},
|
|
159
|
+
}));
|
|
160
|
+
return {
|
|
161
|
+
next_cursor: data.next_offset || null,
|
|
162
|
+
next: null,
|
|
163
|
+
previous: null,
|
|
164
|
+
nfts,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Hook to fetch NFT collectibles from Simdune API.
|
|
169
|
+
* Returns data in SimpleHash format for compatibility with AccountAssets component.
|
|
170
|
+
* @param address - Wallet address to fetch collectibles for
|
|
171
|
+
* @param chainIdsParam - Optional array of chain IDs to filter by
|
|
172
|
+
* @param options - Optional parameters (limit, filterSpam)
|
|
173
|
+
*/
|
|
174
|
+
export function useSimCollectibles(address, chainIdsParam, options) {
|
|
175
|
+
return useQuery({
|
|
176
|
+
queryKey: ["simCollectibles", address, chainIdsParam, options],
|
|
177
|
+
queryFn: async () => {
|
|
178
|
+
if (!address)
|
|
179
|
+
throw new Error("Address is required");
|
|
180
|
+
const data = await fetchSimCollectibles(address, chainIdsParam, options);
|
|
181
|
+
return transformToSimpleHashFormat(data);
|
|
182
|
+
},
|
|
183
|
+
enabled: Boolean(address),
|
|
184
|
+
staleTime: 30 * 1000,
|
|
185
|
+
gcTime: 5 * 60 * 1000,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const SIMDUNE_API_HOST = "https://simdune-api.sean-430.workers.dev";
|
|
2
|
+
/**
|
|
3
|
+
* Builds a Simdune API URL with the proxy wrapper and optional dev mode key.
|
|
4
|
+
* @param endpoint - The Simdune API endpoint (e.g., "/v1/evm/balances/0x...")
|
|
5
|
+
* @param queryParams - Optional URLSearchParams to append
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildSimduneUrl(endpoint: string, queryParams?: URLSearchParams): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const SIMDUNE_API_HOST = "https://simdune-api.sean-430.workers.dev";
|
|
2
|
+
/**
|
|
3
|
+
* Builds a Simdune API URL with the proxy wrapper and optional dev mode key.
|
|
4
|
+
* @param endpoint - The Simdune API endpoint (e.g., "/v1/evm/balances/0x...")
|
|
5
|
+
* @param queryParams - Optional URLSearchParams to append
|
|
6
|
+
*/
|
|
7
|
+
export function buildSimduneUrl(endpoint, queryParams) {
|
|
8
|
+
const baseUrl = `${SIMDUNE_API_HOST}/?url=https://api.sim.dune.com${endpoint}`;
|
|
9
|
+
let url = baseUrl;
|
|
10
|
+
if (queryParams && queryParams.toString()) {
|
|
11
|
+
url += `?${queryParams.toString()}`;
|
|
12
|
+
}
|
|
13
|
+
if (process.env.NEXT_PUBLIC_DEVMODE_SHARED_SECRET) {
|
|
14
|
+
url += `${queryParams?.toString() ? "&" : "?"}localkey=${process.env.NEXT_PUBLIC_DEVMODE_SHARED_SECRET}`;
|
|
15
|
+
}
|
|
16
|
+
return url;
|
|
17
|
+
}
|
|
@@ -31,6 +31,7 @@ export { useRemoveSessionKey } from "./useRemoveSessionKey";
|
|
|
31
31
|
export { useRouter } from "./useRouter";
|
|
32
32
|
export { useSearchParamsSSR } from "./useSearchParamsSSR";
|
|
33
33
|
export { useSimBalance, useSimSvmBalance, useSimTokenBalance } from "./useSimBalance";
|
|
34
|
+
export { useSimCollectibles } from "./useSimCollectibles";
|
|
34
35
|
export { useSiwe } from "./useSiwe";
|
|
35
36
|
export { useTokenBalance } from "./useTokenBalance";
|
|
36
37
|
export { useTokenBalanceDirect } from "./useTokenBalanceDirect";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { SimpleHashNFTResponse } from "@b3dotfun/sdk/global-account/types/simplehash.types";
|
|
2
|
+
export interface SimCollectibleMetadata {
|
|
3
|
+
uri: string;
|
|
4
|
+
attributes?: Array<{
|
|
5
|
+
trait_type: string;
|
|
6
|
+
value: string;
|
|
7
|
+
display_type?: string | null;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export interface SimCollectibleEntry {
|
|
11
|
+
contract_address: string;
|
|
12
|
+
token_standard: "ERC721" | "ERC1155";
|
|
13
|
+
token_id: string;
|
|
14
|
+
chain: string;
|
|
15
|
+
chain_id: number;
|
|
16
|
+
name?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
symbol?: string;
|
|
19
|
+
image_url?: string;
|
|
20
|
+
last_sale_price?: string;
|
|
21
|
+
metadata?: SimCollectibleMetadata;
|
|
22
|
+
is_spam?: boolean;
|
|
23
|
+
spam_score?: number;
|
|
24
|
+
explanations?: string[];
|
|
25
|
+
balance?: string;
|
|
26
|
+
last_acquired?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface SimCollectiblesResponse {
|
|
29
|
+
address: string;
|
|
30
|
+
entries: SimCollectibleEntry[];
|
|
31
|
+
next_offset?: string;
|
|
32
|
+
request_time: string;
|
|
33
|
+
response_time: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Hook to fetch NFT collectibles from Simdune API.
|
|
37
|
+
* Returns data in SimpleHash format for compatibility with AccountAssets component.
|
|
38
|
+
* @param address - Wallet address to fetch collectibles for
|
|
39
|
+
* @param chainIdsParam - Optional array of chain IDs to filter by
|
|
40
|
+
* @param options - Optional parameters (limit, filterSpam)
|
|
41
|
+
*/
|
|
42
|
+
export declare function useSimCollectibles(address?: string, chainIdsParam?: number[], options?: {
|
|
43
|
+
limit?: number;
|
|
44
|
+
filterSpam?: boolean;
|
|
45
|
+
}): import("@tanstack/react-query").UseQueryResult<SimpleHashNFTResponse, Error>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const SIMDUNE_API_HOST = "https://simdune-api.sean-430.workers.dev";
|
|
2
|
+
/**
|
|
3
|
+
* Builds a Simdune API URL with the proxy wrapper and optional dev mode key.
|
|
4
|
+
* @param endpoint - The Simdune API endpoint (e.g., "/v1/evm/balances/0x...")
|
|
5
|
+
* @param queryParams - Optional URLSearchParams to append
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildSimduneUrl(endpoint: string, queryParams?: URLSearchParams): string;
|
package/package.json
CHANGED
|
@@ -38,6 +38,11 @@ export function AccountAssets({ nfts, isLoading }: AccountAssetsProps) {
|
|
|
38
38
|
const [expandedCollections, setExpandedCollections] = useState<Set<string>>(
|
|
39
39
|
() => new Set(collections.map(c => c.collection_id)),
|
|
40
40
|
);
|
|
41
|
+
const [failedImages, setFailedImages] = useState<Set<string>>(() => new Set());
|
|
42
|
+
|
|
43
|
+
const handleImageError = (imageId: string) => {
|
|
44
|
+
setFailedImages(prev => new Set(prev).add(imageId));
|
|
45
|
+
};
|
|
41
46
|
|
|
42
47
|
if (isLoading) {
|
|
43
48
|
return (
|
|
@@ -72,7 +77,7 @@ export function AccountAssets({ nfts, isLoading }: AccountAssetsProps) {
|
|
|
72
77
|
};
|
|
73
78
|
|
|
74
79
|
return (
|
|
75
|
-
<div className="flex flex-col gap-3
|
|
80
|
+
<div className="flex flex-col gap-3">
|
|
76
81
|
{collections.map(collection => {
|
|
77
82
|
const isExpanded = expandedCollections.has(collection.collection_id);
|
|
78
83
|
|
|
@@ -84,11 +89,12 @@ export function AccountAssets({ nfts, isLoading }: AccountAssetsProps) {
|
|
|
84
89
|
className="flex w-full items-center justify-between"
|
|
85
90
|
>
|
|
86
91
|
<div className="flex items-center gap-1">
|
|
87
|
-
{collection.collection_image && (
|
|
92
|
+
{collection.collection_image && !failedImages.has(`collection-${collection.collection_id}`) && (
|
|
88
93
|
<img
|
|
89
94
|
src={collection.collection_image}
|
|
90
95
|
alt={collection.collection_name}
|
|
91
96
|
className="h-5 w-5 shrink-0 rounded object-cover"
|
|
97
|
+
onError={() => handleImageError(`collection-${collection.collection_id}`)}
|
|
92
98
|
/>
|
|
93
99
|
)}
|
|
94
100
|
<p className="font-neue-montreal-medium text-[14px] text-[#3f3f46]">
|
|
@@ -115,17 +121,23 @@ export function AccountAssets({ nfts, isLoading }: AccountAssetsProps) {
|
|
|
115
121
|
{isExpanded && (
|
|
116
122
|
<div className="flex gap-3 overflow-x-auto">
|
|
117
123
|
{collection.nfts.map(nft => (
|
|
118
|
-
<div
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
<div
|
|
125
|
+
key={nft.nft_id}
|
|
126
|
+
className="bg-b3-react-muted relative h-[98px] w-[98px] shrink-0 overflow-hidden rounded-lg"
|
|
127
|
+
>
|
|
128
|
+
{!failedImages.has(nft.nft_id) && (
|
|
129
|
+
<img
|
|
130
|
+
src={
|
|
131
|
+
nft.previews?.image_medium_url ||
|
|
132
|
+
nft.extra_metadata?.image_original_url ||
|
|
133
|
+
nft.collection?.image_url ||
|
|
134
|
+
""
|
|
135
|
+
}
|
|
136
|
+
alt={nft.name || "NFT"}
|
|
137
|
+
className="h-full w-full object-cover"
|
|
138
|
+
onError={() => handleImageError(nft.nft_id)}
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
129
141
|
</div>
|
|
130
142
|
))}
|
|
131
143
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useActiveWallet } from "thirdweb/react";
|
|
2
2
|
import { AccountAssets } from "..";
|
|
3
|
-
import {
|
|
3
|
+
import { useSimCollectibles } from "../../hooks";
|
|
4
4
|
|
|
5
5
|
const NFTContent = () => {
|
|
6
6
|
// Get active wallet state
|
|
@@ -8,12 +8,12 @@ const NFTContent = () => {
|
|
|
8
8
|
const activeAccount = activeWallet?.getAccount();
|
|
9
9
|
const activeAddress = activeAccount?.address;
|
|
10
10
|
|
|
11
|
-
const { data: nfts, isLoading } =
|
|
11
|
+
const { data: nfts, isLoading } = useSimCollectibles(activeAddress, [1, 8453], { filterSpam: true });
|
|
12
12
|
|
|
13
13
|
return (
|
|
14
14
|
<div style={{ minHeight: "100px" }}>
|
|
15
|
-
{nfts
|
|
16
|
-
<AccountAssets nfts={nfts
|
|
15
|
+
{nfts ? (
|
|
16
|
+
<AccountAssets nfts={nfts} isLoading={isLoading} />
|
|
17
17
|
) : (
|
|
18
18
|
<div className="py-12 text-center text-gray-500">No NFTs found</div>
|
|
19
19
|
)}
|
|
@@ -40,6 +40,7 @@ export { useRemoveSessionKey } from "./useRemoveSessionKey";
|
|
|
40
40
|
export { useRouter } from "./useRouter";
|
|
41
41
|
export { useSearchParamsSSR } from "./useSearchParamsSSR";
|
|
42
42
|
export { useSimBalance, useSimSvmBalance, useSimTokenBalance } from "./useSimBalance";
|
|
43
|
+
export { useSimCollectibles } from "./useSimCollectibles";
|
|
43
44
|
export { useSiwe } from "./useSiwe";
|
|
44
45
|
export { useTokenBalance } from "./useTokenBalance";
|
|
45
46
|
export { useTokenBalanceDirect } from "./useTokenBalanceDirect";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { buildSimduneUrl } from "../utils/simdune";
|
|
2
3
|
|
|
3
4
|
export interface SimTokenMetadata {
|
|
4
5
|
logo?: string;
|
|
@@ -60,11 +61,12 @@ async function fetchSimBalance(address: string, chainIdsParam: number[]): Promis
|
|
|
60
61
|
if (!address) throw new Error("Address is required");
|
|
61
62
|
|
|
62
63
|
const chainIds = chainIdsParam.length === 0 ? "mainnet" : chainIdsParam.join(",");
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
const queryParams = new URLSearchParams();
|
|
65
|
+
queryParams.append("metadata", "logo");
|
|
66
|
+
queryParams.append("chain_ids", chainIds);
|
|
67
|
+
queryParams.append("exclude_spam_tokens", "true");
|
|
67
68
|
|
|
69
|
+
const url = buildSimduneUrl(`/v1/evm/balances/${address}`, queryParams);
|
|
68
70
|
const response = await fetch(url);
|
|
69
71
|
|
|
70
72
|
if (!response.ok) {
|
|
@@ -84,11 +86,10 @@ async function fetchSimTokenBalance(
|
|
|
84
86
|
if (!tokenAddress) throw new Error("Token address is required");
|
|
85
87
|
if (!chainId) throw new Error("Chain ID is required");
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
url += `&localkey=${process.env.NEXT_PUBLIC_DEVMODE_SHARED_SECRET}`;
|
|
90
|
-
}
|
|
89
|
+
const queryParams = new URLSearchParams();
|
|
90
|
+
queryParams.append("chain_ids", chainId.toString());
|
|
91
91
|
|
|
92
|
+
const url = buildSimduneUrl(`/v1/evm/balances/${walletAddress}/token/${tokenAddress}`, queryParams);
|
|
92
93
|
const response = await fetch(url);
|
|
93
94
|
|
|
94
95
|
if (!response.ok) {
|
|
@@ -110,14 +111,7 @@ async function fetchSimSvmBalance(address: string, chains?: string[], limit?: nu
|
|
|
110
111
|
queryParams.append("limit", limit.toString());
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
if (queryParams.toString()) {
|
|
115
|
-
url += `?${queryParams.toString()}`;
|
|
116
|
-
}
|
|
117
|
-
if (process.env.NEXT_PUBLIC_DEVMODE_SHARED_SECRET) {
|
|
118
|
-
url += `${queryParams.toString() ? "&" : "?"}localkey=${process.env.NEXT_PUBLIC_DEVMODE_SHARED_SECRET}`;
|
|
119
|
-
}
|
|
120
|
-
|
|
114
|
+
const url = buildSimduneUrl(`/beta/svm/balances/${address}`, queryParams.toString() ? queryParams : undefined);
|
|
121
115
|
const response = await fetch(url);
|
|
122
116
|
|
|
123
117
|
if (!response.ok) {
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type { NFT, SimpleHashNFTResponse } from "@b3dotfun/sdk/global-account/types/simplehash.types";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { buildSimduneUrl } from "../utils/simdune";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Safely parse a balance string to a number, capping at MAX_SAFE_INTEGER
|
|
7
|
+
* to prevent overflow issues with large ERC1155 balances.
|
|
8
|
+
*/
|
|
9
|
+
function safeParseBalance(balance: string | undefined): number {
|
|
10
|
+
if (!balance) return 1;
|
|
11
|
+
const parsed = parseInt(balance, 10);
|
|
12
|
+
if (Number.isNaN(parsed)) return 1;
|
|
13
|
+
return Math.min(parsed, Number.MAX_SAFE_INTEGER);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Simdune Collectibles types
|
|
17
|
+
export interface SimCollectibleMetadata {
|
|
18
|
+
uri: string;
|
|
19
|
+
attributes?: Array<{
|
|
20
|
+
trait_type: string;
|
|
21
|
+
value: string;
|
|
22
|
+
display_type?: string | null;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SimCollectibleEntry {
|
|
27
|
+
contract_address: string;
|
|
28
|
+
token_standard: "ERC721" | "ERC1155";
|
|
29
|
+
token_id: string;
|
|
30
|
+
chain: string;
|
|
31
|
+
chain_id: number;
|
|
32
|
+
name?: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
symbol?: string;
|
|
35
|
+
image_url?: string;
|
|
36
|
+
last_sale_price?: string;
|
|
37
|
+
metadata?: SimCollectibleMetadata;
|
|
38
|
+
is_spam?: boolean;
|
|
39
|
+
spam_score?: number;
|
|
40
|
+
explanations?: string[];
|
|
41
|
+
balance?: string;
|
|
42
|
+
last_acquired?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SimCollectiblesResponse {
|
|
46
|
+
address: string;
|
|
47
|
+
entries: SimCollectibleEntry[];
|
|
48
|
+
next_offset?: string;
|
|
49
|
+
request_time: string;
|
|
50
|
+
response_time: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function fetchSimCollectibles(
|
|
54
|
+
address: string,
|
|
55
|
+
chainIdsParam?: number[],
|
|
56
|
+
options?: { limit?: number; filterSpam?: boolean },
|
|
57
|
+
): Promise<SimCollectiblesResponse> {
|
|
58
|
+
if (!address) throw new Error("Address is required");
|
|
59
|
+
|
|
60
|
+
const queryParams = new URLSearchParams();
|
|
61
|
+
if (chainIdsParam && chainIdsParam.length > 0) {
|
|
62
|
+
queryParams.append("chain_ids", chainIdsParam.join(","));
|
|
63
|
+
}
|
|
64
|
+
if (options?.limit) {
|
|
65
|
+
queryParams.append("limit", options.limit.toString());
|
|
66
|
+
}
|
|
67
|
+
if (options?.filterSpam !== undefined) {
|
|
68
|
+
queryParams.append("filter_spam", options.filterSpam.toString());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const url = buildSimduneUrl(`/v1/evm/collectibles/${address}`, queryParams);
|
|
72
|
+
const response = await fetch(url);
|
|
73
|
+
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new Error(`Failed to fetch collectibles: ${response.statusText}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const data: SimCollectiblesResponse = await response.json();
|
|
79
|
+
return data;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Transforms Simdune collectibles response to SimpleHash NFT format
|
|
84
|
+
* for compatibility with existing AccountAssets component
|
|
85
|
+
*/
|
|
86
|
+
function transformToSimpleHashFormat(data: SimCollectiblesResponse): SimpleHashNFTResponse {
|
|
87
|
+
const nfts: NFT[] = data.entries.map(entry => ({
|
|
88
|
+
nft_id: `${entry.chain}.${entry.contract_address}.${entry.token_id}`,
|
|
89
|
+
chain: entry.chain,
|
|
90
|
+
contract_address: entry.contract_address,
|
|
91
|
+
token_id: entry.token_id,
|
|
92
|
+
name: entry.name || "",
|
|
93
|
+
description: entry.description || "",
|
|
94
|
+
previews: {
|
|
95
|
+
image_small_url: entry.image_url || "",
|
|
96
|
+
image_medium_url: entry.image_url || "",
|
|
97
|
+
image_large_url: entry.image_url || "",
|
|
98
|
+
image_opengraph_url: entry.image_url || "",
|
|
99
|
+
blurhash: "",
|
|
100
|
+
predominant_color: "",
|
|
101
|
+
},
|
|
102
|
+
image_url: entry.image_url || "",
|
|
103
|
+
image_properties: {
|
|
104
|
+
width: 0,
|
|
105
|
+
height: 0,
|
|
106
|
+
size: 0,
|
|
107
|
+
mime_type: "",
|
|
108
|
+
exif_orientation: null,
|
|
109
|
+
},
|
|
110
|
+
video_url: null,
|
|
111
|
+
video_properties: null,
|
|
112
|
+
audio_url: null,
|
|
113
|
+
audio_properties: null,
|
|
114
|
+
model_url: null,
|
|
115
|
+
model_properties: null,
|
|
116
|
+
other_url: null,
|
|
117
|
+
other_properties: null,
|
|
118
|
+
background_color: null,
|
|
119
|
+
external_url: null,
|
|
120
|
+
created_date: "",
|
|
121
|
+
status: "minted",
|
|
122
|
+
token_count: 1,
|
|
123
|
+
owner_count: 1,
|
|
124
|
+
owners: [
|
|
125
|
+
{
|
|
126
|
+
owner_address: data.address,
|
|
127
|
+
quantity: safeParseBalance(entry.balance),
|
|
128
|
+
quantity_string: entry.balance || "1",
|
|
129
|
+
first_acquired_date: entry.last_acquired || "",
|
|
130
|
+
last_acquired_date: entry.last_acquired || "",
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
contract: {
|
|
134
|
+
type: entry.token_standard,
|
|
135
|
+
name: entry.name || "",
|
|
136
|
+
symbol: entry.symbol || null,
|
|
137
|
+
deployed_by: "",
|
|
138
|
+
deployed_via_contract: "",
|
|
139
|
+
owned_by: "",
|
|
140
|
+
has_multiple_collections: false,
|
|
141
|
+
},
|
|
142
|
+
collection: {
|
|
143
|
+
collection_id: entry.contract_address,
|
|
144
|
+
name: entry.symbol || "Unknown Collection",
|
|
145
|
+
description: null,
|
|
146
|
+
image_url: entry.image_url || "",
|
|
147
|
+
image_properties: {
|
|
148
|
+
width: 0,
|
|
149
|
+
height: 0,
|
|
150
|
+
mime_type: "",
|
|
151
|
+
},
|
|
152
|
+
banner_image_url: null,
|
|
153
|
+
category: null,
|
|
154
|
+
is_nsfw: null,
|
|
155
|
+
external_url: null,
|
|
156
|
+
twitter_username: null,
|
|
157
|
+
discord_url: null,
|
|
158
|
+
instagram_username: null,
|
|
159
|
+
medium_username: null,
|
|
160
|
+
telegram_url: null,
|
|
161
|
+
marketplace_pages: [],
|
|
162
|
+
metaplex_mint: null,
|
|
163
|
+
metaplex_candy_machine: null,
|
|
164
|
+
metaplex_first_verified_creator: null,
|
|
165
|
+
floor_prices: [],
|
|
166
|
+
top_bids: [],
|
|
167
|
+
distinct_owner_count: 0,
|
|
168
|
+
distinct_nft_count: 0,
|
|
169
|
+
total_quantity: 0,
|
|
170
|
+
chains: [entry.chain],
|
|
171
|
+
top_contracts: [entry.contract_address],
|
|
172
|
+
collection_royalties: [],
|
|
173
|
+
},
|
|
174
|
+
last_sale: entry.last_sale_price
|
|
175
|
+
? {
|
|
176
|
+
price: entry.last_sale_price,
|
|
177
|
+
}
|
|
178
|
+
: null,
|
|
179
|
+
primary_sale: null,
|
|
180
|
+
first_created: {
|
|
181
|
+
minted_to: data.address,
|
|
182
|
+
quantity: 1,
|
|
183
|
+
quantity_string: "1",
|
|
184
|
+
timestamp: "",
|
|
185
|
+
block_number: 0,
|
|
186
|
+
transaction: "",
|
|
187
|
+
transaction_initiator: "",
|
|
188
|
+
},
|
|
189
|
+
rarity: {
|
|
190
|
+
rank: null,
|
|
191
|
+
score: null,
|
|
192
|
+
unique_attributes: null,
|
|
193
|
+
},
|
|
194
|
+
royalty: [],
|
|
195
|
+
extra_metadata: {
|
|
196
|
+
attributes: (entry.metadata?.attributes || []).map(attr => ({
|
|
197
|
+
trait_type: attr.trait_type,
|
|
198
|
+
value: attr.value,
|
|
199
|
+
display_type: attr.display_type ?? null,
|
|
200
|
+
})),
|
|
201
|
+
image_original_url: entry.image_url || "",
|
|
202
|
+
animation_original_url: null,
|
|
203
|
+
metadata_original_url: entry.metadata?.uri || "",
|
|
204
|
+
},
|
|
205
|
+
}));
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
next_cursor: data.next_offset || null,
|
|
209
|
+
next: null,
|
|
210
|
+
previous: null,
|
|
211
|
+
nfts,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Hook to fetch NFT collectibles from Simdune API.
|
|
217
|
+
* Returns data in SimpleHash format for compatibility with AccountAssets component.
|
|
218
|
+
* @param address - Wallet address to fetch collectibles for
|
|
219
|
+
* @param chainIdsParam - Optional array of chain IDs to filter by
|
|
220
|
+
* @param options - Optional parameters (limit, filterSpam)
|
|
221
|
+
*/
|
|
222
|
+
export function useSimCollectibles(
|
|
223
|
+
address?: string,
|
|
224
|
+
chainIdsParam?: number[],
|
|
225
|
+
options?: { limit?: number; filterSpam?: boolean },
|
|
226
|
+
) {
|
|
227
|
+
return useQuery({
|
|
228
|
+
queryKey: ["simCollectibles", address, chainIdsParam, options],
|
|
229
|
+
queryFn: async () => {
|
|
230
|
+
if (!address) throw new Error("Address is required");
|
|
231
|
+
const data = await fetchSimCollectibles(address, chainIdsParam, options);
|
|
232
|
+
return transformToSimpleHashFormat(data);
|
|
233
|
+
},
|
|
234
|
+
enabled: Boolean(address),
|
|
235
|
+
staleTime: 30 * 1000,
|
|
236
|
+
gcTime: 5 * 60 * 1000,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const SIMDUNE_API_HOST = "https://simdune-api.sean-430.workers.dev";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds a Simdune API URL with the proxy wrapper and optional dev mode key.
|
|
5
|
+
* @param endpoint - The Simdune API endpoint (e.g., "/v1/evm/balances/0x...")
|
|
6
|
+
* @param queryParams - Optional URLSearchParams to append
|
|
7
|
+
*/
|
|
8
|
+
export function buildSimduneUrl(endpoint: string, queryParams?: URLSearchParams): string {
|
|
9
|
+
const baseUrl = `${SIMDUNE_API_HOST}/?url=https://api.sim.dune.com${endpoint}`;
|
|
10
|
+
|
|
11
|
+
let url = baseUrl;
|
|
12
|
+
if (queryParams && queryParams.toString()) {
|
|
13
|
+
url += `?${queryParams.toString()}`;
|
|
14
|
+
}
|
|
15
|
+
if (process.env.NEXT_PUBLIC_DEVMODE_SHARED_SECRET) {
|
|
16
|
+
url += `${queryParams?.toString() ? "&" : "?"}localkey=${process.env.NEXT_PUBLIC_DEVMODE_SHARED_SECRET}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return url;
|
|
20
|
+
}
|