From e269cddd70e3558cc3b232258ce7298658ef501b Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Tue, 16 Dec 2025 06:16:13 +0700 Subject: [PATCH 1/5] Add wallet portfolio fetching and display to user table Introduces a new hook `useWalletPortfolio` to fetch and aggregate wallet token balances and USD values across multiple chains. Updates the user wallets table to include total balance and token columns, a chain selector, a fetch balances button with progress, and summary stats for funded wallets and total value. Enhances the analytics/stat component to support empty text display. --- .../src/@/components/analytics/stat.tsx | 5 +- .../user-wallets-table.tsx | 284 ++++++++++++++++-- .../src/@/hooks/useWalletPortfolio.ts | 153 ++++++++++ 3 files changed, 420 insertions(+), 22 deletions(-) create mode 100644 apps/dashboard/src/@/hooks/useWalletPortfolio.ts diff --git a/apps/dashboard/src/@/components/analytics/stat.tsx b/apps/dashboard/src/@/components/analytics/stat.tsx index 7d856cd4c9e..abc2f062504 100644 --- a/apps/dashboard/src/@/components/analytics/stat.tsx +++ b/apps/dashboard/src/@/components/analytics/stat.tsx @@ -6,13 +6,16 @@ export const StatCard: React.FC<{ icon: React.FC<{ className?: string }>; formatter?: (value: number) => string; isPending: boolean; -}> = ({ label, value, formatter, icon: Icon, isPending }) => { + emptyText?: string; +}> = ({ label, value, formatter, icon: Icon, isPending, emptyText }) => { return (
{isPending ? ( + ) : emptyText ? ( + {emptyText} ) : value !== undefined && formatter ? ( formatter(value) ) : ( diff --git a/apps/dashboard/src/@/components/in-app-wallet-users-content/user-wallets-table.tsx b/apps/dashboard/src/@/components/in-app-wallet-users-content/user-wallets-table.tsx index b15b56ab62a..d1ec7fca29d 100644 --- a/apps/dashboard/src/@/components/in-app-wallet-users-content/user-wallets-table.tsx +++ b/apps/dashboard/src/@/components/in-app-wallet-users-content/user-wallets-table.tsx @@ -2,14 +2,25 @@ import { createColumnHelper } from "@tanstack/react-table"; import { format } from "date-fns"; -import { ArrowLeftIcon, ArrowRightIcon, UserIcon } from "lucide-react"; +import { + ArrowLeftIcon, + ArrowRightIcon, + DollarSignIcon, + RefreshCwIcon, + UserIcon, + WalletIcon, +} from "lucide-react"; import Papa from "papaparse"; import { useCallback, useMemo, useState } from "react"; import type { ThirdwebClient } from "thirdweb"; import type { WalletUser } from "thirdweb/wallets"; +import { StatCard } from "@/components/analytics/stat"; +import { MultiNetworkSelector } from "@/components/blocks/NetworkSelectors"; import { TWTable } from "@/components/blocks/TWTable"; import { WalletAddress } from "@/components/blocks/wallet-address"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Progress } from "@/components/ui/progress"; import { Spinner } from "@/components/ui/Spinner"; import { ToolTipLabel, @@ -22,6 +33,10 @@ import { useAllEmbeddedWallets, useEmbeddedWallets, } from "@/hooks/useEmbeddedWallets"; +import { + useFetchAllPortfolios, + type WalletPortfolioData, +} from "@/hooks/useWalletPortfolio"; import { CopyTextButton } from "../ui/CopyTextButton"; import { AdvancedSearchInput } from "./AdvancedSearchInput"; import { SearchResults } from "./SearchResults"; @@ -74,6 +89,100 @@ export function UserWalletsTable( | { ecosystemSlug: string; projectClientId?: never } ), ) { + const [activePage, setActivePage] = useState(1); + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [hasSearchResults, setHasSearchResults] = useState(false); + + // Portfolio state + const [selectedChains, setSelectedChains] = useState([1]); // Default to Ethereum + const [portfolioMap, setPortfolioMap] = useState< + Map + >(new Map()); + const [portfolioLoaded, setPortfolioLoaded] = useState(false); + const [fetchProgress, setFetchProgress] = useState({ + completed: 0, + total: 0, + }); + + const walletsQuery = useEmbeddedWallets({ + authToken: props.authToken, + clientId: props.projectClientId, + ecosystemSlug: props.ecosystemSlug, + teamId: props.teamId, + page: activePage, + }); + const wallets = walletsQuery?.data?.users || []; + const { mutateAsync: getAllEmbeddedWallets, isPending: isLoadingAllWallets } = + useAllEmbeddedWallets({ + authToken: props.authToken, + }); + + const fetchPortfoliosMutation = useFetchAllPortfolios(); + + const handleFetchBalances = useCallback(async () => { + if (selectedChains.length === 0) return; + + try { + // First get all wallets + const allWallets = await getAllEmbeddedWallets({ + clientId: props.projectClientId, + ecosystemSlug: props.ecosystemSlug, + teamId: props.teamId, + }); + + const allAddresses = allWallets + .map((w) => w.wallets[0]?.address) + .filter((a): a is string => !!a); + + if (allAddresses.length === 0) { + setPortfolioLoaded(true); + return; + } + + setFetchProgress({ completed: 0, total: allAddresses.length }); + + const results = await fetchPortfoliosMutation.mutateAsync({ + addresses: allAddresses, + client: props.client, + chainIds: selectedChains, + authToken: props.authToken, + onProgress: (completed, total) => { + setFetchProgress({ completed, total }); + }, + }); + + setPortfolioMap(results); + setPortfolioLoaded(true); + } catch (error) { + console.error("Failed to fetch balances:", error); + } + }, [ + selectedChains, + getAllEmbeddedWallets, + props.projectClientId, + props.ecosystemSlug, + props.teamId, + props.client, + props.authToken, + fetchPortfoliosMutation, + ]); + + const isFetchingBalances = + isLoadingAllWallets || fetchPortfoliosMutation.isPending; + + const aggregatedStats = useMemo(() => { + let fundedWallets = 0; + let totalValue = 0; + portfolioMap.forEach((data) => { + if (data.totalUsdValue > 0) { + fundedWallets++; + totalValue += data.totalUsdValue; + } + }); + return { fundedWallets, totalValue }; + }, [portfolioMap]); + const columns = useMemo(() => { return [ columnHelper.accessor("id", { @@ -129,6 +238,79 @@ export function UserWalletsTable( header: "Address", id: "address", }), + columnHelper.accessor("wallets", { + id: "total_balance", + header: "Total Balance", + cell: (cell) => { + const address = cell.getValue()[0]?.address; + if (!address) return "N/A"; + if (!portfolioLoaded) { + return ; + } + const data = portfolioMap.get(address); + if (!data) { + return ; + } + return ( + + {new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(data.totalUsdValue)} + + ); + }, + }), + columnHelper.accessor("wallets", { + id: "tokens", + header: "Tokens", + cell: (cell) => { + const address = cell.getValue()[0]?.address; + if (!address) return "N/A"; + if (!portfolioLoaded) { + return ; + } + const data = portfolioMap.get(address); + if (!data || data.tokens.length === 0) { + return None; + } + + const topTokens = data.tokens + .sort((a, b) => (b.usdValue || 0) - (a.usdValue || 0)) + .slice(0, 3) + .map((t) => t.symbol) + .join(", "); + + return ( + + + + {topTokens} + {data.tokens.length > 3 ? "..." : ""} + + +
+ {data.tokens.map((t) => ( +
+ {t.symbol} + + {new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(t.usdValue || 0)} + +
+ ))} +
+
+
+
+ ); + }, + }), columnHelper.accessor("linkedAccounts", { cell: (cell) => { const email = getPrimaryEmail(cell.getValue()); @@ -201,24 +383,7 @@ export function UserWalletsTable( id: "login_methods", }), ]; - }, [props.client]); - - const [activePage, setActivePage] = useState(1); - const [searchResults, setSearchResults] = useState([]); - const [isSearching, setIsSearching] = useState(false); - const [hasSearchResults, setHasSearchResults] = useState(false); - const walletsQuery = useEmbeddedWallets({ - authToken: props.authToken, - clientId: props.projectClientId, - ecosystemSlug: props.ecosystemSlug, - teamId: props.teamId, - page: activePage, - }); - const wallets = walletsQuery?.data?.users || []; - const { mutateAsync: getAllEmbeddedWallets, isPending } = - useAllEmbeddedWallets({ - authToken: props.authToken, - }); + }, [props.client, portfolioMap, portfolioLoaded]); const handleSearch = async (searchType: SearchType, query: string) => { setIsSearching(true); @@ -315,11 +480,11 @@ export function UserWalletsTable(
@@ -330,6 +495,83 @@ export function UserWalletsTable( ) : ( <> + {/* Chain Selector and Fetch Button */} +
+
+ + +
+ + {isFetchingBalances && ( +
+ {fetchProgress.total > 0 && ( + + )} +

+ This may take a few minutes +

+
+ )} + + {portfolioLoaded && !isFetchingBalances && ( + + Balances loaded for {portfolioMap.size} wallets + + )} +
+ + {/* Stats Section */} +
+ + + new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(value) + } + isPending={isFetchingBalances} + emptyText={!portfolioLoaded ? "—" : undefined} + /> +
{ + const results = await Promise.all( + chainIds.map(async (chainId) => { + try { + const url = new URL( + `https://api.thirdweb.com/v1/wallets/${address}/tokens`, + ); + url.searchParams.set("chainId", chainId.toString()); + url.searchParams.set("limit", "50"); + url.searchParams.set("metadata", "true"); + url.searchParams.set("includeSpam", "false"); + url.searchParams.set("includeNative", "true"); + url.searchParams.set("sortBy", "usd_value"); + url.searchParams.set("sortOrder", "desc"); + + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + "x-client-id": client.clientId, + }, + }); + + if (!response.ok) { + return []; + } + + const data = await response.json(); + const rawTokens = data.result?.tokens || []; + + return rawTokens.map( + (t: { + balance: string; + decimals: number; + name: string; + symbol: string; + token_address: string; + chain_id: number; + price_data?: { usd_value?: number; price_usd?: number }; + }): WalletPortfolioToken => ({ + value: BigInt(t.balance), + decimals: t.decimals, + displayValue: (Number(t.balance) / 10 ** t.decimals).toString(), + name: t.name, + symbol: t.symbol, + tokenAddress: t.token_address, + chainId: t.chain_id, + usdValue: t.price_data?.usd_value, + priceUsd: t.price_data?.price_usd, + }), + ); + } catch (e) { + console.error( + `Failed to fetch tokens for ${address} on chain ${chainId}:`, + e, + ); + return []; + } + }), + ); + + const allTokens = results.flat(); + const totalUsdValue = allTokens.reduce( + (acc, token) => acc + (token.usdValue || 0), + 0, + ); + + return { + totalUsdValue, + tokens: allTokens, + }; +} + +// Batch fetch portfolios for all addresses with progress callback +export async function fetchAllPortfolios( + addresses: string[], + client: ThirdwebClient, + chainIds: number[], + authToken: string, + onProgress?: (completed: number, total: number) => void, +): Promise> { + const results = new Map(); + const batchSize = 5; // Process 5 addresses at a time to avoid rate limits + + for (let i = 0; i < addresses.length; i += batchSize) { + const batch = addresses.slice(i, i + batchSize); + const batchResults = await Promise.all( + batch.map(async (address) => { + const data = await fetchWalletPortfolio( + address, + client, + chainIds, + authToken, + ); + return { address, data }; + }), + ); + + for (const { address, data } of batchResults) { + results.set(address, data); + } + + onProgress?.(Math.min(i + batchSize, addresses.length), addresses.length); + } + + return results; +} + +export function useFetchAllPortfolios() { + return useMutation({ + mutationFn: async ({ + addresses, + client, + chainIds, + authToken, + onProgress, + }: { + addresses: string[]; + client: ThirdwebClient; + chainIds: number[]; + authToken: string; + onProgress?: (completed: number, total: number) => void; + }) => { + return fetchAllPortfolios( + addresses, + client, + chainIds, + authToken, + onProgress, + ); + }, + }); +} From dd85e3197db480e13f7e0c2f0629569278fa3856 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Tue, 16 Dec 2025 06:22:10 +0700 Subject: [PATCH 2/5] lint --- apps/dashboard/src/@/hooks/useWalletPortfolio.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dashboard/src/@/hooks/useWalletPortfolio.ts b/apps/dashboard/src/@/hooks/useWalletPortfolio.ts index 7a21ce513ec..dd99323d834 100644 --- a/apps/dashboard/src/@/hooks/useWalletPortfolio.ts +++ b/apps/dashboard/src/@/hooks/useWalletPortfolio.ts @@ -2,7 +2,7 @@ import { useMutation } from "@tanstack/react-query"; import type { ThirdwebClient } from "thirdweb"; import type { GetBalanceResult } from "thirdweb/extensions/erc20"; -export type WalletPortfolioToken = GetBalanceResult & { +type WalletPortfolioToken = GetBalanceResult & { usdValue?: number; priceUsd?: number; }; @@ -92,7 +92,7 @@ async function fetchWalletPortfolio( } // Batch fetch portfolios for all addresses with progress callback -export async function fetchAllPortfolios( +async function fetchAllPortfolios( addresses: string[], client: ThirdwebClient, chainIds: number[], From 090f0d908427da2b933c7ff5ca96e23a05bfa668 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Tue, 16 Dec 2025 19:03:28 +0700 Subject: [PATCH 3/5] Improve wallet hooks with retry and batching logic Added retry logic with exponential backoff to embedded wallet and portfolio fetching to handle transient errors and rate limits. Increased batch size and used Promise.allSettled for concurrent portfolio fetches, improving throughput and resilience. Partial results are now returned on repeated failures, and error handling is more robust. --- .../src/@/hooks/useEmbeddedWallets.ts | 82 +++++--- .../src/@/hooks/useWalletPortfolio.ts | 179 ++++++++++++------ 2 files changed, 173 insertions(+), 88 deletions(-) diff --git a/apps/dashboard/src/@/hooks/useEmbeddedWallets.ts b/apps/dashboard/src/@/hooks/useEmbeddedWallets.ts index 20ab3a0e34a..95d93599cf5 100644 --- a/apps/dashboard/src/@/hooks/useEmbeddedWallets.ts +++ b/apps/dashboard/src/@/hooks/useEmbeddedWallets.ts @@ -177,33 +177,63 @@ export function useAllEmbeddedWallets(params: { authToken: string }) { }) => { const responses: WalletUser[] = []; let page = 1; - - while (true) { - const res = await queryClient.fetchQuery<{ - users: WalletUser[]; - hasMore: boolean; - }>({ - queryFn: fetchAccountList({ - clientId, - ecosystemSlug, - teamId, - jwt: authToken, - pageNumber: page, - }), - queryKey: embeddedWalletsKeys.embeddedWallets( - address || "", - clientId || ecosystemSlug || "", - page, - ), - }); - - responses.push(...res.users); - - if (!res.hasMore) { - break; + let consecutiveFailures = 0; + const maxConsecutiveFailures = 3; + + while (consecutiveFailures < maxConsecutiveFailures) { + try { + const res = await queryClient.fetchQuery<{ + users: WalletUser[]; + hasMore: boolean; + }>({ + queryFn: fetchAccountList({ + clientId, + ecosystemSlug, + teamId, + jwt: authToken, + pageNumber: page, + }), + queryKey: embeddedWalletsKeys.embeddedWallets( + address || "", + clientId || ecosystemSlug || "", + page, + ), + retry: 3, + retryDelay: (attemptIndex) => + Math.min(1000 * 2 ** attemptIndex, 10000), + }); + + responses.push(...res.users); + consecutiveFailures = 0; // Reset on success + + if (!res.hasMore) { + break; + } + + page++; + } catch (error) { + consecutiveFailures++; + console.warn( + `Failed to fetch page ${page}, attempt ${consecutiveFailures}/${maxConsecutiveFailures}:`, + error, + ); + + if (consecutiveFailures >= maxConsecutiveFailures) { + // If we have some data, return it instead of throwing + if (responses.length > 0) { + console.warn( + `Returning partial results (${responses.length} users) after ${maxConsecutiveFailures} consecutive failures`, + ); + break; + } + throw error; + } + + // Wait before retrying the same page + await new Promise((resolve) => + setTimeout(resolve, 1000 * consecutiveFailures), + ); } - - page++; } return responses; diff --git a/apps/dashboard/src/@/hooks/useWalletPortfolio.ts b/apps/dashboard/src/@/hooks/useWalletPortfolio.ts index dd99323d834..774ee87e70a 100644 --- a/apps/dashboard/src/@/hooks/useWalletPortfolio.ts +++ b/apps/dashboard/src/@/hooks/useWalletPortfolio.ts @@ -12,6 +12,102 @@ export type WalletPortfolioData = { tokens: WalletPortfolioToken[]; }; +// Retry helper with exponential backoff +async function fetchWithRetry( + url: string, + options: RequestInit, + maxRetries = 3, +): Promise { + let lastError: Error | null = null; + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + const response = await fetch(url, options); + + // Retry on rate limit (429) or server errors (5xx) + if (response.status === 429 || response.status >= 500) { + const delay = Math.min(1000 * 2 ** attempt, 10000); // Max 10s delay + await new Promise((resolve) => setTimeout(resolve, delay)); + continue; + } + + return response; + } catch (e) { + lastError = e instanceof Error ? e : new Error(String(e)); + // Network error - retry with backoff + const delay = Math.min(1000 * 2 ** attempt, 10000); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + + throw lastError || new Error("Max retries exceeded"); +} + +// Fetch tokens for a single address on a single chain +async function fetchTokensForChain( + address: string, + chainId: number, + client: ThirdwebClient, + authToken: string, +): Promise { + try { + const url = new URL( + `https://api.thirdweb.com/v1/wallets/${address}/tokens`, + ); + url.searchParams.set("chainId", chainId.toString()); + url.searchParams.set("limit", "50"); + url.searchParams.set("metadata", "true"); + url.searchParams.set("includeSpam", "false"); + url.searchParams.set("includeNative", "true"); + url.searchParams.set("sortBy", "usd_value"); + url.searchParams.set("sortOrder", "desc"); + + const response = await fetchWithRetry(url.toString(), { + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + "x-client-id": client.clientId, + }, + }); + + if (!response.ok) { + return []; + } + + const data = await response.json(); + const rawTokens = data.result?.tokens || []; + + return rawTokens.map( + (t: { + balance: string; + decimals: number; + name: string; + symbol: string; + token_address: string; + chain_id: number; + price_data?: { usd_value?: number; price_usd?: number }; + }): WalletPortfolioToken => ({ + value: BigInt(t.balance), + decimals: t.decimals, + displayValue: (Number(t.balance) / 10 ** t.decimals).toString(), + name: t.name, + symbol: t.symbol, + tokenAddress: t.token_address, + chainId: t.chain_id, + usdValue: t.price_data?.usd_value, + priceUsd: t.price_data?.price_usd, + }), + ); + } catch (e) { + // Silent fail for individual chain - continue with others + console.warn( + `Failed to fetch tokens for ${address} on chain ${chainId}:`, + e, + ); + return []; + } +} + // Fetch tokens for a single address on selected chains async function fetchWalletPortfolio( address: string, @@ -19,64 +115,11 @@ async function fetchWalletPortfolio( chainIds: number[], authToken: string, ): Promise { + // Fetch all chains concurrently for this address const results = await Promise.all( - chainIds.map(async (chainId) => { - try { - const url = new URL( - `https://api.thirdweb.com/v1/wallets/${address}/tokens`, - ); - url.searchParams.set("chainId", chainId.toString()); - url.searchParams.set("limit", "50"); - url.searchParams.set("metadata", "true"); - url.searchParams.set("includeSpam", "false"); - url.searchParams.set("includeNative", "true"); - url.searchParams.set("sortBy", "usd_value"); - url.searchParams.set("sortOrder", "desc"); - - const response = await fetch(url.toString(), { - headers: { - Authorization: `Bearer ${authToken}`, - "Content-Type": "application/json", - "x-client-id": client.clientId, - }, - }); - - if (!response.ok) { - return []; - } - - const data = await response.json(); - const rawTokens = data.result?.tokens || []; - - return rawTokens.map( - (t: { - balance: string; - decimals: number; - name: string; - symbol: string; - token_address: string; - chain_id: number; - price_data?: { usd_value?: number; price_usd?: number }; - }): WalletPortfolioToken => ({ - value: BigInt(t.balance), - decimals: t.decimals, - displayValue: (Number(t.balance) / 10 ** t.decimals).toString(), - name: t.name, - symbol: t.symbol, - tokenAddress: t.token_address, - chainId: t.chain_id, - usdValue: t.price_data?.usd_value, - priceUsd: t.price_data?.price_usd, - }), - ); - } catch (e) { - console.error( - `Failed to fetch tokens for ${address} on chain ${chainId}:`, - e, - ); - return []; - } - }), + chainIds.map((chainId) => + fetchTokensForChain(address, chainId, client, authToken), + ), ); const allTokens = results.flat(); @@ -100,11 +143,14 @@ async function fetchAllPortfolios( onProgress?: (completed: number, total: number) => void, ): Promise> { const results = new Map(); - const batchSize = 5; // Process 5 addresses at a time to avoid rate limits + const batchSize = 10; // Process 10 addresses at a time for better throughput + let completed = 0; for (let i = 0; i < addresses.length; i += batchSize) { const batch = addresses.slice(i, i + batchSize); - const batchResults = await Promise.all( + + // Process batch concurrently with individual error handling + const batchResults = await Promise.allSettled( batch.map(async (address) => { const data = await fetchWalletPortfolio( address, @@ -116,11 +162,20 @@ async function fetchAllPortfolios( }), ); - for (const { address, data } of batchResults) { - results.set(address, data); + // Only add successful results + for (const result of batchResults) { + if (result.status === "fulfilled") { + results.set(result.value.address, result.value.data); + } } - onProgress?.(Math.min(i + batchSize, addresses.length), addresses.length); + completed = Math.min(i + batchSize, addresses.length); + onProgress?.(completed, addresses.length); + + // Small delay between batches to avoid overwhelming the API + if (i + batchSize < addresses.length) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } } return results; From 890ff960fc776514917d1d1070684140173c3c68 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Tue, 16 Dec 2025 19:24:14 +0700 Subject: [PATCH 4/5] Refactor wallet portfolio fetch to use team and ecosystem IDs Replaces the use of the client object with explicit teamId, clientId, and ecosystemSlug parameters for fetching wallet portfolios. Updates headers and function signatures to support these changes, improving clarity and flexibility in API requests. --- .../user-wallets-table.tsx | 5 ++- .../src/@/hooks/useWalletPortfolio.ts | 44 ++++++++++++++----- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/apps/dashboard/src/@/components/in-app-wallet-users-content/user-wallets-table.tsx b/apps/dashboard/src/@/components/in-app-wallet-users-content/user-wallets-table.tsx index d1ec7fca29d..0ec5f42e30d 100644 --- a/apps/dashboard/src/@/components/in-app-wallet-users-content/user-wallets-table.tsx +++ b/apps/dashboard/src/@/components/in-app-wallet-users-content/user-wallets-table.tsx @@ -144,9 +144,11 @@ export function UserWalletsTable( const results = await fetchPortfoliosMutation.mutateAsync({ addresses: allAddresses, - client: props.client, chainIds: selectedChains, authToken: props.authToken, + teamId: props.teamId, + clientId: props.projectClientId, + ecosystemSlug: props.ecosystemSlug, onProgress: (completed, total) => { setFetchProgress({ completed, total }); }, @@ -163,7 +165,6 @@ export function UserWalletsTable( props.projectClientId, props.ecosystemSlug, props.teamId, - props.client, props.authToken, fetchPortfoliosMutation, ]); diff --git a/apps/dashboard/src/@/hooks/useWalletPortfolio.ts b/apps/dashboard/src/@/hooks/useWalletPortfolio.ts index 774ee87e70a..538de57150d 100644 --- a/apps/dashboard/src/@/hooks/useWalletPortfolio.ts +++ b/apps/dashboard/src/@/hooks/useWalletPortfolio.ts @@ -1,5 +1,4 @@ import { useMutation } from "@tanstack/react-query"; -import type { ThirdwebClient } from "thirdweb"; import type { GetBalanceResult } from "thirdweb/extensions/erc20"; type WalletPortfolioToken = GetBalanceResult & { @@ -47,8 +46,10 @@ async function fetchWithRetry( async function fetchTokensForChain( address: string, chainId: number, - client: ThirdwebClient, authToken: string, + teamId: string, + clientId?: string, + ecosystemSlug?: string, ): Promise { try { const url = new URL( @@ -66,7 +67,11 @@ async function fetchTokensForChain( headers: { Authorization: `Bearer ${authToken}`, "Content-Type": "application/json", - "x-client-id": client.clientId, + "x-thirdweb-team-id": teamId, + ...(clientId && { "x-client-id": clientId }), + ...(ecosystemSlug && { + "x-ecosystem-id": `ecosystem.${ecosystemSlug}`, + }), }, }); @@ -111,14 +116,23 @@ async function fetchTokensForChain( // Fetch tokens for a single address on selected chains async function fetchWalletPortfolio( address: string, - client: ThirdwebClient, chainIds: number[], authToken: string, + teamId: string, + clientId?: string, + ecosystemSlug?: string, ): Promise { // Fetch all chains concurrently for this address const results = await Promise.all( chainIds.map((chainId) => - fetchTokensForChain(address, chainId, client, authToken), + fetchTokensForChain( + address, + chainId, + authToken, + teamId, + clientId, + ecosystemSlug, + ), ), ); @@ -137,9 +151,11 @@ async function fetchWalletPortfolio( // Batch fetch portfolios for all addresses with progress callback async function fetchAllPortfolios( addresses: string[], - client: ThirdwebClient, chainIds: number[], authToken: string, + teamId: string, + clientId?: string, + ecosystemSlug?: string, onProgress?: (completed: number, total: number) => void, ): Promise> { const results = new Map(); @@ -154,9 +170,11 @@ async function fetchAllPortfolios( batch.map(async (address) => { const data = await fetchWalletPortfolio( address, - client, chainIds, authToken, + teamId, + clientId, + ecosystemSlug, ); return { address, data }; }), @@ -185,22 +203,28 @@ export function useFetchAllPortfolios() { return useMutation({ mutationFn: async ({ addresses, - client, chainIds, authToken, + teamId, + clientId, + ecosystemSlug, onProgress, }: { addresses: string[]; - client: ThirdwebClient; chainIds: number[]; authToken: string; + teamId: string; + clientId?: string; + ecosystemSlug?: string; onProgress?: (completed: number, total: number) => void; }) => { return fetchAllPortfolios( addresses, - client, chainIds, authToken, + teamId, + clientId, + ecosystemSlug, onProgress, ); }, From 3792da56c2d92c52afc093d66af9bf9d34f09ccc Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Wed, 17 Dec 2025 05:26:07 +0700 Subject: [PATCH 5/5] dynamic dev host --- apps/dashboard/src/@/hooks/useWalletPortfolio.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/dashboard/src/@/hooks/useWalletPortfolio.ts b/apps/dashboard/src/@/hooks/useWalletPortfolio.ts index 538de57150d..adba6d2a992 100644 --- a/apps/dashboard/src/@/hooks/useWalletPortfolio.ts +++ b/apps/dashboard/src/@/hooks/useWalletPortfolio.ts @@ -1,5 +1,6 @@ import { useMutation } from "@tanstack/react-query"; import type { GetBalanceResult } from "thirdweb/extensions/erc20"; +import { THIRDWEB_API_HOST } from "@/constants/urls"; type WalletPortfolioToken = GetBalanceResult & { usdValue?: number; @@ -52,9 +53,7 @@ async function fetchTokensForChain( ecosystemSlug?: string, ): Promise { try { - const url = new URL( - `https://api.thirdweb.com/v1/wallets/${address}/tokens`, - ); + const url = new URL(`${THIRDWEB_API_HOST}/v1/wallets/${address}/tokens`); url.searchParams.set("chainId", chainId.toString()); url.searchParams.set("limit", "50"); url.searchParams.set("metadata", "true");