From 67229ab7c42577ae01bf4ed775b86c1633cfe088 Mon Sep 17 00:00:00 2001 From: Konstantin Fastov Date: Thu, 31 Oct 2024 15:54:18 +0700 Subject: [PATCH 01/11] feat: add draft asset portfolio implementation --- app/[addressOrDomain]/page.tsx | 107 ++++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 9 deletions(-) diff --git a/app/[addressOrDomain]/page.tsx b/app/[addressOrDomain]/page.tsx index a41c9386..c3d8f3c5 100644 --- a/app/[addressOrDomain]/page.tsx +++ b/app/[addressOrDomain]/page.tsx @@ -55,6 +55,14 @@ type DebtStatus = { }[]; }; +// Protocols to exclude from asset percentages +const EXCLUDED_PROTOCOL_IDS = [ + '8e4c3af5-f9bc-4646-9b66-3cd7bf7c8aec', // zkLend + '19a6474c-17a9-4d61-95ab-5f768cd01485', // Nimbora + '6203cd68-991a-4bae-a18a-ff42207ce813', // Vesu + // TODO: Add more protocols or add better exclusion logic (ban all protocols?) +]; + export default function Page({ params }: AddressOrDomainProps) { const router = useRouter(); const addressOrDomain = params.addressOrDomain; @@ -189,18 +197,99 @@ export default function Page({ params }: AddressOrDomainProps) { setQuestsLoading(false); }, []); + const calculateAssetPercentages = async (userTokens: ArgentUserToken[], tokens: ArgentTokenMap) => { + // First get dapps data + const dapps = await fetchDapps(); + + let totalValue = 0; + const assetValues: { [symbol: string]: number } = {}; + + // First pass: calculate total value and individual token values + for await (const token of userTokens) { + const tokenInfo = tokens[token.tokenAddress]; + if (!tokenInfo || token.tokenBalance === "0") continue; + + console.log(tokenInfo); + + if (tokenInfo.dappId) { + console.log('Dapp info for token', tokenInfo.name, { + dappName: dapps[tokenInfo.dappId]?.name, + dappId: tokenInfo.dappId, + dappDetails: dapps[tokenInfo.dappId] + }); + } + + // Skip tokens that belong to staking protocols + if (tokenInfo.dappId && EXCLUDED_PROTOCOL_IDS.includes(tokenInfo.dappId)) { + continue; + } + + const value = await calculateTokenPrice( + token.tokenAddress, + tokenToDecimal(token.tokenBalance, tokenInfo.decimals), + "USD" + ); + + const symbol = tokenInfo.symbol || "Unknown"; + assetValues[symbol] = (assetValues[symbol] || 0) + value; + totalValue += value; + } + + // Convert to percentages and format + const sortedAssets = Object.entries(assetValues) + .sort(([, a], [, b]) => b - a) + .map(([symbol, value]) => ({ + itemLabel: symbol, + itemValue: ((value / totalValue) * 100).toFixed(2), + itemValueSymbol: "%", + color: "" // Colors will be assigned later + })); + + console.log(sortedAssets.slice()); + + // Handle "Others" category if needed + if (sortedAssets.length > 4) { + const others = sortedAssets.slice(4).reduce( + (sum, asset) => sum + parseFloat(asset.itemValue), + 0 + ); + sortedAssets.splice(4); + sortedAssets.push({ + itemLabel: "Others", + itemValue: others.toFixed(2), + itemValueSymbol: "%", + color: "" + }); + } + + // Assign colors + const colors = ["#1E2097", "#637DEB", "#2775CA", "#5CE3FE", "#F4FAFF"]; + sortedAssets.forEach((asset, index) => { + asset.color = colors[index]; + }); + + return sortedAssets; + }; + const fetchPortfolioAssets = useCallback(async (addr: string) => { + setLoadingProtocols(true); + try { + const [dapps, tokens, userTokens, userDapps] = await Promise.all([ + fetchDapps(), + fetchTokens(), + fetchUserTokens(addr), + fetchUserDapps(addr) + ]); - // TODO: Implement fetch from Argent API - const assets = [ - { color: "#1E2097", itemLabel: "USDC", itemValue: "46.68", itemValueSymbol: "%" }, - { color: "#637DEB", itemLabel: "USDT", itemValue: "27.94", itemValueSymbol: "%" }, - { color: "#2775CA", itemLabel: "STRK", itemValue: "22.78", itemValueSymbol: "%" }, - { color: "#5CE3FE", itemLabel: "ETH", itemValue: "0.36", itemValueSymbol: "%" }, - { color: "#F4FAFF", itemLabel: "Others", itemValue: "2.36", itemValueSymbol: "%" }, - ]; - setPortfolioAssets(assets); + if (!tokens || !userTokens) return; + const assets = await calculateAssetPercentages(userTokens, tokens); + setPortfolioAssets(assets); + } catch (error) { + showNotification("Error while fetching portfolio assets", "error"); + console.log("Error while fetching portfolio assets", error); + } + setLoadingProtocols(false); }, []); const userHasDebt = (userDapps: ArgentUserDapp[]) => { From 77da829dde7ac50c3e49143357eb3fa64be16091 Mon Sep 17 00:00:00 2001 From: Konstantin Fastov Date: Thu, 31 Oct 2024 17:58:04 +0700 Subject: [PATCH 02/11] refactor: fetch portfolio data once for both charts --- app/[addressOrDomain]/page.tsx | 87 +++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/app/[addressOrDomain]/page.tsx b/app/[addressOrDomain]/page.tsx index c3d8f3c5..974ebe82 100644 --- a/app/[addressOrDomain]/page.tsx +++ b/app/[addressOrDomain]/page.tsx @@ -197,10 +197,7 @@ export default function Page({ params }: AddressOrDomainProps) { setQuestsLoading(false); }, []); - const calculateAssetPercentages = async (userTokens: ArgentUserToken[], tokens: ArgentTokenMap) => { - // First get dapps data - const dapps = await fetchDapps(); - + const calculateAssetPercentages = async (userTokens: ArgentUserToken[], tokens: ArgentTokenMap, dapps: ArgentDappMap) => { let totalValue = 0; const assetValues: { [symbol: string]: number } = {}; @@ -271,25 +268,21 @@ export default function Page({ params }: AddressOrDomainProps) { return sortedAssets; }; - const fetchPortfolioAssets = useCallback(async (addr: string) => { - setLoadingProtocols(true); + const fetchPortfolioAssets = useCallback(async (data: { + dapps: ArgentDappMap, + tokens: ArgentTokenMap, + userTokens: ArgentUserToken[], + userDapps: ArgentUserDapp[] + }) => { + const { dapps, tokens, userTokens, userDapps } = data; try { - const [dapps, tokens, userTokens, userDapps] = await Promise.all([ - fetchDapps(), - fetchTokens(), - fetchUserTokens(addr), - fetchUserDapps(addr) - ]); - if (!tokens || !userTokens) return; - - const assets = await calculateAssetPercentages(userTokens, tokens); + const assets = await calculateAssetPercentages(userTokens, tokens, dapps); setPortfolioAssets(assets); } catch (error) { showNotification("Error while fetching portfolio assets", "error"); console.log("Error while fetching portfolio assets", error); } - setLoadingProtocols(false); }, []); const userHasDebt = (userDapps: ArgentUserDapp[]) => { @@ -403,25 +396,15 @@ export default function Page({ params }: AddressOrDomainProps) { }); } - const fetchPortfolioProtocols = useCallback(async (addr: string) => { - let dapps: ArgentDappMap = {}; - let tokens: ArgentTokenMap = {}; - let userTokens: ArgentUserToken[] = []; - let userDapps: ArgentUserDapp[] = []; - - setLoadingProtocols(true); - try { - [dapps, tokens, userTokens, userDapps] = await Promise.all([ - fetchDapps(), - fetchTokens(), - fetchUserTokens(addr), - fetchUserDapps(addr) - ]); - } catch (error) { - showNotification("Error while fetching address portfolio", "error"); - console.log("Error while fetching address portfolio", error); - } + const fetchPortfolioProtocols = useCallback(async (data: { + dapps: ArgentDappMap, + tokens: ArgentTokenMap, + userTokens: ArgentUserToken[], + userDapps: ArgentUserDapp[] + }) => { + const { dapps, tokens, userTokens, userDapps } = data; + // TODO correct this if (!dapps || !tokens || (!userTokens && !userDapps)) return; let protocolsMap: ChartItemMap = {}; @@ -439,16 +422,44 @@ export default function Page({ params }: AddressOrDomainProps) { showNotification("Error while calculating address portfolio stats", "error"); console.log("Error while calculating address portfolio stats", error); } + }, [address]); - setLoadingProtocols(false); - }, []); + const fetchPortfolioData = useCallback(async (addr: string) => { + setLoadingProtocols(true); + try { + const [dappsData, tokensData, userTokensData, userDappsData] = + await Promise.all([ + fetchDapps(), + fetchTokens(), + fetchUserTokens(addr), + fetchUserDapps(addr), + ]); + + const data = { + dapps: dappsData, + tokens: tokensData, + userTokens: userTokensData, + userDapps: userDappsData, + }; + + await Promise.all([ + fetchPortfolioProtocols(data), + fetchPortfolioAssets(data), + ]); + } catch (error) { + showNotification("Error while fetching address portfolio", "error"); + console.log("Error while fetching address portfolio", error); + } finally { + setLoadingProtocols(false); + } + }, [fetchPortfolioProtocols, fetchPortfolioAssets]); useEffect(() => { if (!identity) return; + console.log("Fetching data for identity", identity.owner); fetchQuestData(identity.owner); fetchPageData(identity.owner); - fetchPortfolioAssets(identity.owner); - fetchPortfolioProtocols(identity.owner); + fetchPortfolioData(identity.owner); }, [identity]); useEffect(() => setNotFound(false), [dynamicRoute]); From d189ee79e22ec7c20518dfb878556b26fbacf821 Mon Sep 17 00:00:00 2001 From: Konstantin Fastov Date: Thu, 31 Oct 2024 21:03:54 +0700 Subject: [PATCH 03/11] fix: better percentage in portfolio assets chart --- app/[addressOrDomain]/page.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/[addressOrDomain]/page.tsx b/app/[addressOrDomain]/page.tsx index 974ebe82..774f91b2 100644 --- a/app/[addressOrDomain]/page.tsx +++ b/app/[addressOrDomain]/page.tsx @@ -216,20 +216,20 @@ export default function Page({ params }: AddressOrDomainProps) { }); } - // Skip tokens that belong to staking protocols - if (tokenInfo.dappId && EXCLUDED_PROTOCOL_IDS.includes(tokenInfo.dappId)) { - continue; - } - const value = await calculateTokenPrice( token.tokenAddress, tokenToDecimal(token.tokenBalance, tokenInfo.decimals), "USD" ); - const symbol = tokenInfo.symbol || "Unknown"; - assetValues[symbol] = (assetValues[symbol] || 0) + value; + // Add to total value regardless of protocol totalValue += value; + + // Only add to asset breakdown if not from excluded protocol + if (!tokenInfo.dappId || !EXCLUDED_PROTOCOL_IDS.includes(tokenInfo.dappId)) { + const symbol = tokenInfo.symbol || "Unknown"; + assetValues[symbol] = (assetValues[symbol] || 0) + value; + } } // Convert to percentages and format From c8237133edab6def46d86f3c2095f9a3c4e721e7 Mon Sep 17 00:00:00 2001 From: Konstantin Fastov Date: Fri, 1 Nov 2024 18:47:13 +0700 Subject: [PATCH 04/11] feat: include assets from protocols in portfolio asset chart (WiP) --- app/[addressOrDomain]/page.tsx | 63 ++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/app/[addressOrDomain]/page.tsx b/app/[addressOrDomain]/page.tsx index 774f91b2..8e71d9dc 100644 --- a/app/[addressOrDomain]/page.tsx +++ b/app/[addressOrDomain]/page.tsx @@ -197,8 +197,15 @@ export default function Page({ params }: AddressOrDomainProps) { setQuestsLoading(false); }, []); - const calculateAssetPercentages = async (userTokens: ArgentUserToken[], tokens: ArgentTokenMap, dapps: ArgentDappMap) => { + const calculateAssetPercentages = async ( + userTokens: ArgentUserToken[], + tokens: ArgentTokenMap, + dapps: ArgentDappMap, + userDapps: ArgentUserDapp[], + ) => { let totalValue = 0; + let totalTokenValue = 0; + let totalProtocolValue = 0; const assetValues: { [symbol: string]: number } = {}; // First pass: calculate total value and individual token values @@ -216,6 +223,11 @@ export default function Page({ params }: AddressOrDomainProps) { }); } + // Skip tokens from excluded protocols + if (tokenInfo.dappId && EXCLUDED_PROTOCOL_IDS.includes(tokenInfo.dappId)) { + continue; + } + const value = await calculateTokenPrice( token.tokenAddress, tokenToDecimal(token.tokenBalance, tokenInfo.decimals), @@ -224,14 +236,52 @@ export default function Page({ params }: AddressOrDomainProps) { // Add to total value regardless of protocol totalValue += value; + totalTokenValue += value; + console.log('Added token value:', value, 'from token:', tokenInfo.symbol); // Only add to asset breakdown if not from excluded protocol - if (!tokenInfo.dappId || !EXCLUDED_PROTOCOL_IDS.includes(tokenInfo.dappId)) { - const symbol = tokenInfo.symbol || "Unknown"; - assetValues[symbol] = (assetValues[symbol] || 0) + value; + const symbol = tokenInfo.symbol || "Unknown"; + assetValues[symbol] = (assetValues[symbol] || 0) + value; + } + + // Process tokens from userDapps + for await (const userDapp of userDapps) { + const positions = userDapp.products[0]?.positions; + if (!positions) continue; + + for (const position of positions) { + const totalBalances = position.totalBalances; + for (const tokenAddress of Object.keys(totalBalances)) { + const tokenBalance = totalBalances[tokenAddress]; + const tokenInfo = tokens[tokenAddress]; + if (!tokenInfo) continue; + if (tokenBalance === "0") continue; + + const value = await calculateTokenPrice( + tokenAddress, + tokenToDecimal(tokenBalance, tokenInfo.decimals), + "USD" + ); + + totalProtocolValue += value; + + // Don't add negative balances to total value (or may have percentages > 100%) + if (value < 0) continue; + // Add to total value regardless of protocol + totalValue += value; + console.log('Added protocol value:', value, 'from token:', tokenInfo.symbol, 'protocol:', dapps[userDapp.dappId].name); + // Only add to asset breakdown if not from excluded protocol. Restaking anyone? + if (tokenInfo.dappId && EXCLUDED_PROTOCOL_IDS.includes(tokenInfo.dappId)) { + continue; + } + const symbol = tokenInfo.symbol || "Unknown"; + assetValues[symbol] = (assetValues[symbol] || 0) + value; + } } } + console.log("!!!Total value:", totalValue, "Total token value:", totalTokenValue, "Total protocol value:", totalProtocolValue); + // Convert to percentages and format const sortedAssets = Object.entries(assetValues) .sort(([, a], [, b]) => b - a) @@ -242,6 +292,7 @@ export default function Page({ params }: AddressOrDomainProps) { color: "" // Colors will be assigned later })); + console.log(JSON.stringify(sortedAssets, null, 2)); console.log(sortedAssets.slice()); // Handle "Others" category if needed @@ -276,8 +327,9 @@ export default function Page({ params }: AddressOrDomainProps) { }) => { const { dapps, tokens, userTokens, userDapps } = data; try { + // TODO: correct this condition if (!tokens || !userTokens) return; - const assets = await calculateAssetPercentages(userTokens, tokens, dapps); + const assets = await calculateAssetPercentages(userTokens, tokens, dapps, userDapps); setPortfolioAssets(assets); } catch (error) { showNotification("Error while fetching portfolio assets", "error"); @@ -404,7 +456,6 @@ export default function Page({ params }: AddressOrDomainProps) { }) => { const { dapps, tokens, userTokens, userDapps } = data; - // TODO correct this if (!dapps || !tokens || (!userTokens && !userDapps)) return; let protocolsMap: ChartItemMap = {}; From cbaa6d766316210af2f727806b3a23351906ad3a Mon Sep 17 00:00:00 2001 From: Konstantin Fastov Date: Sun, 3 Nov 2024 16:15:40 +0700 Subject: [PATCH 05/11] refactor: remove debug logs, disable protocol assets display --- app/[addressOrDomain]/page.tsx | 40 ++++++------------------------ services/argentPortfolioService.ts | 10 +++++++- 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/app/[addressOrDomain]/page.tsx b/app/[addressOrDomain]/page.tsx index 8e71d9dc..2cd0b74b 100644 --- a/app/[addressOrDomain]/page.tsx +++ b/app/[addressOrDomain]/page.tsx @@ -18,7 +18,7 @@ import ProfileCardSkeleton from "@components/skeletons/profileCardSkeleton"; import { getDataFromId } from "@services/starknetIdService"; import { usePathname, useRouter } from "next/navigation"; import ErrorScreen from "@components/UI/screens/errorScreen"; -import { ArgentDappMap, ArgentTokenMap, ArgentUserDapp, ArgentUserToken, CompletedQuests } from "../../types/backTypes"; +import { ArgentDappMap, ArgentToken, ArgentTokenMap, ArgentUserDapp, ArgentUserToken, CompletedQuests } from "../../types/backTypes"; import QuestSkeleton from "@components/skeletons/questsSkeleton"; import QuestCardCustomised from "@components/dashboard/CustomisedQuestCard"; import QuestStyles from "@styles/Home.module.css"; @@ -55,14 +55,6 @@ type DebtStatus = { }[]; }; -// Protocols to exclude from asset percentages -const EXCLUDED_PROTOCOL_IDS = [ - '8e4c3af5-f9bc-4646-9b66-3cd7bf7c8aec', // zkLend - '19a6474c-17a9-4d61-95ab-5f768cd01485', // Nimbora - '6203cd68-991a-4bae-a18a-ff42207ce813', // Vesu - // TODO: Add more protocols or add better exclusion logic (ban all protocols?) -]; - export default function Page({ params }: AddressOrDomainProps) { const router = useRouter(); const addressOrDomain = params.addressOrDomain; @@ -213,18 +205,8 @@ export default function Page({ params }: AddressOrDomainProps) { const tokenInfo = tokens[token.tokenAddress]; if (!tokenInfo || token.tokenBalance === "0") continue; - console.log(tokenInfo); - + // Skip protocol tokens (like LPT pair tokens, staking, etc.) if (tokenInfo.dappId) { - console.log('Dapp info for token', tokenInfo.name, { - dappName: dapps[tokenInfo.dappId]?.name, - dappId: tokenInfo.dappId, - dappDetails: dapps[tokenInfo.dappId] - }); - } - - // Skip tokens from excluded protocols - if (tokenInfo.dappId && EXCLUDED_PROTOCOL_IDS.includes(tokenInfo.dappId)) { continue; } @@ -237,7 +219,6 @@ export default function Page({ params }: AddressOrDomainProps) { // Add to total value regardless of protocol totalValue += value; totalTokenValue += value; - console.log('Added token value:', value, 'from token:', tokenInfo.symbol); // Only add to asset breakdown if not from excluded protocol const symbol = tokenInfo.symbol || "Unknown"; @@ -269,9 +250,8 @@ export default function Page({ params }: AddressOrDomainProps) { if (value < 0) continue; // Add to total value regardless of protocol totalValue += value; - console.log('Added protocol value:', value, 'from token:', tokenInfo.symbol, 'protocol:', dapps[userDapp.dappId].name); - // Only add to asset breakdown if not from excluded protocol. Restaking anyone? - if (tokenInfo.dappId && EXCLUDED_PROTOCOL_IDS.includes(tokenInfo.dappId)) { + // Only add to asset breakdown if the token is not from a protocol itself + if (tokenInfo.dappId) { continue; } const symbol = tokenInfo.symbol || "Unknown"; @@ -280,8 +260,6 @@ export default function Page({ params }: AddressOrDomainProps) { } } - console.log("!!!Total value:", totalValue, "Total token value:", totalTokenValue, "Total protocol value:", totalProtocolValue); - // Convert to percentages and format const sortedAssets = Object.entries(assetValues) .sort(([, a], [, b]) => b - a) @@ -292,9 +270,6 @@ export default function Page({ params }: AddressOrDomainProps) { color: "" // Colors will be assigned later })); - console.log(JSON.stringify(sortedAssets, null, 2)); - console.log(sortedAssets.slice()); - // Handle "Others" category if needed if (sortedAssets.length > 4) { const others = sortedAssets.slice(4).reduce( @@ -478,12 +453,14 @@ export default function Page({ params }: AddressOrDomainProps) { const fetchPortfolioData = useCallback(async (addr: string) => { setLoadingProtocols(true); try { + // Argent API requires lowercase address + const normalizedAddr = addr.toLowerCase(); const [dappsData, tokensData, userTokensData, userDappsData] = await Promise.all([ fetchDapps(), fetchTokens(), - fetchUserTokens(addr), - fetchUserDapps(addr), + fetchUserTokens(normalizedAddr), + fetchUserDapps(normalizedAddr), ]); const data = { @@ -507,7 +484,6 @@ export default function Page({ params }: AddressOrDomainProps) { useEffect(() => { if (!identity) return; - console.log("Fetching data for identity", identity.owner); fetchQuestData(identity.owner); fetchPageData(identity.owner); fetchPortfolioData(identity.owner); diff --git a/services/argentPortfolioService.ts b/services/argentPortfolioService.ts index cbff1719..e80d679b 100644 --- a/services/argentPortfolioService.ts +++ b/services/argentPortfolioService.ts @@ -56,7 +56,15 @@ export const calculateTokenPrice = async ( tokenAmount: string, currency: "USD" | "EUR" | "GBP" = "USD" ) => { - const data = await fetchData(`${API_BASE}/${API_VERSION}/tokens/prices/${tokenAddress}?chain=starknet¤cy=${currency}`); + let data: ArgentTokenValue; + try { + data = await fetchData(`${API_BASE}/${API_VERSION}/tokens/prices/${tokenAddress}?chain=starknet¤cy=${currency}`); + } catch (err) { + if (err instanceof Error && err.message.startsWith('Error 404:')) { + return 0; + } + throw err; + } try { return Number(tokenAmount) * Number(data.ccyValue); } catch (err) { From cbe5fe52357b99abaa0c7bd4a8d7aed6f9bebf76 Mon Sep 17 00:00:00 2001 From: Konstantin Fastov Date: Sun, 3 Nov 2024 17:10:51 +0700 Subject: [PATCH 06/11] refactor: better error filtering in price fetch --- services/argentPortfolioService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/argentPortfolioService.ts b/services/argentPortfolioService.ts index e80d679b..ef3925f8 100644 --- a/services/argentPortfolioService.ts +++ b/services/argentPortfolioService.ts @@ -60,7 +60,7 @@ export const calculateTokenPrice = async ( try { data = await fetchData(`${API_BASE}/${API_VERSION}/tokens/prices/${tokenAddress}?chain=starknet¤cy=${currency}`); } catch (err) { - if (err instanceof Error && err.message.startsWith('Error 404:')) { + if (err instanceof Error && err.message.includes('No price for token')) { return 0; } throw err; From 883a135f7e8e11a0836270621906c7caec69f0cf Mon Sep 17 00:00:00 2001 From: Konstantin Fastov Date: Sun, 3 Nov 2024 23:11:34 +0700 Subject: [PATCH 07/11] refactor: address review suggestions --- app/[addressOrDomain]/page.tsx | 118 +++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 50 deletions(-) diff --git a/app/[addressOrDomain]/page.tsx b/app/[addressOrDomain]/page.tsx index e7ecbaaf..c1189040 100644 --- a/app/[addressOrDomain]/page.tsx +++ b/app/[addressOrDomain]/page.tsx @@ -208,70 +208,87 @@ export default function Page({ params }: AddressOrDomainProps) { userDapps: ArgentUserDapp[], ) => { let totalValue = 0; - let totalTokenValue = 0; - let totalProtocolValue = 0; const assetValues: { [symbol: string]: number } = {}; - // First pass: calculate total value and individual token values - for await (const token of userTokens) { + // Process user tokens in parallel + const userTokenPromises = userTokens.map(async (token) => { const tokenInfo = tokens[token.tokenAddress]; - if (!tokenInfo || token.tokenBalance === "0") continue; + if (!tokenInfo || token.tokenBalance === "0") return null; // Skip protocol tokens (like LPT pair tokens, staking, etc.) if (tokenInfo.dappId) { - continue; + return null; } + try { const value = await calculateTokenPrice( token.tokenAddress, tokenToDecimal(token.tokenBalance, tokenInfo.decimals), "USD" - ); - - // Add to total value regardless of protocol - totalValue += value; - totalTokenValue += value; + ); + return { + value, + symbol: tokenInfo.symbol || "Unknown", + isProtocolToken: !!tokenInfo.dappId + }; + } catch (err) { + console.log(`Error calculating price for token ${token.tokenAddress}:`, err); + return null; + } + }); - // Only add to asset breakdown if not from excluded protocol - const symbol = tokenInfo.symbol || "Unknown"; - assetValues[symbol] = (assetValues[symbol] || 0) + value; - } + // Flatten userDapps into an array of token balances + const dappBalances = userDapps.flatMap(dapp => + dapp.products[0]?.positions.flatMap(position => + Object.entries(position.totalBalances).map(([tokenAddress, balance]) => ({ + tokenAddress, + balance, + dappId: dapp.dappId + })) + ) ?? [] + ); - // Process tokens from userDapps - for await (const userDapp of userDapps) { - const positions = userDapp.products[0]?.positions; - if (!positions) continue; - - for (const position of positions) { - const totalBalances = position.totalBalances; - for (const tokenAddress of Object.keys(totalBalances)) { - const tokenBalance = totalBalances[tokenAddress]; - const tokenInfo = tokens[tokenAddress]; - if (!tokenInfo) continue; - if (tokenBalance === "0") continue; - - const value = await calculateTokenPrice( - tokenAddress, - tokenToDecimal(tokenBalance, tokenInfo.decimals), - "USD" - ); + // Process all balances in parallel + const balancePromises = dappBalances.map(async ({ tokenAddress, balance, dappId }) => { + const tokenInfo = tokens[tokenAddress]; + if (!tokenInfo || balance === "0") return null; - totalProtocolValue += value; + try { + const value = await calculateTokenPrice( + tokenAddress, + tokenToDecimal(balance, tokenInfo.decimals), + "USD" + ); - // Don't add negative balances to total value (or may have percentages > 100%) - if (value < 0) continue; - // Add to total value regardless of protocol - totalValue += value; - // Only add to asset breakdown if the token is not from a protocol itself - if (tokenInfo.dappId) { - continue; - } - const symbol = tokenInfo.symbol || "Unknown"; - assetValues[symbol] = (assetValues[symbol] || 0) + value; - } + return { + value, + symbol: tokenInfo.symbol || "Unknown", + isProtocolToken: !!tokenInfo.dappId, + }; + } catch (err) { + console.log(`Error calculating price for token ${tokenAddress}:`, err); + return null; } - } + }); + + // Process results + const results = (await Promise.all([ + ...balancePromises, + ...userTokenPromises + ])).filter(Boolean); + results.forEach(result => { + if (!result) return; + const { value, symbol, isProtocolToken } = result; + + if (value < 0) return; // Skip negative balances + + totalValue += value; + + if (!isProtocolToken) { + assetValues[symbol] = (assetValues[symbol] || 0) + value; + } + }); // Convert to percentages and format const sortedAssets = Object.entries(assetValues) .sort(([, a], [, b]) => b - a) @@ -300,9 +317,8 @@ export default function Page({ params }: AddressOrDomainProps) { // Assign colors const colors = ["#1E2097", "#637DEB", "#2775CA", "#5CE3FE", "#F4FAFF"]; sortedAssets.forEach((asset, index) => { - asset.color = colors[index]; + asset.color = colors[index % colors.length]; // Use modulo to recycle colors if needed }); - return sortedAssets; }; @@ -314,8 +330,10 @@ export default function Page({ params }: AddressOrDomainProps) { }) => { const { dapps, tokens, userTokens, userDapps } = data; try { - // TODO: correct this condition - if (!tokens || !userTokens) return; + if (!tokens || !userTokens || !dapps || !userDapps) { + console.warn('Missing required data for portfolio calculation'); + return; + } const assets = await calculateAssetPercentages(userTokens, tokens, dapps, userDapps); setPortfolioAssets(assets); } catch (error) { From b271d596daaf0f27a9be5a3e0f24bf6c43c98056 Mon Sep 17 00:00:00 2001 From: Konstantin Fastov Date: Sun, 3 Nov 2024 23:21:20 +0700 Subject: [PATCH 08/11] fix: fix incorrect merge result --- app/[addressOrDomain]/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/[addressOrDomain]/page.tsx b/app/[addressOrDomain]/page.tsx index c1189040..f1a59202 100644 --- a/app/[addressOrDomain]/page.tsx +++ b/app/[addressOrDomain]/page.tsx @@ -506,6 +506,8 @@ export default function Page({ params }: AddressOrDomainProps) { userTokens: ArgentUserToken[], userDapps: ArgentUserDapp[] }) => { + const { dapps, tokens, userTokens, userDapps } = data; + if (!dapps || !tokens || (!userTokens && !userDapps)) return; let protocolsMap: ChartItemMap = {}; From 8c192aed42c7a16a38e5cd98011f8e4ecc286214 Mon Sep 17 00:00:00 2001 From: Konstantin Fastov Date: Mon, 4 Nov 2024 21:42:36 +0700 Subject: [PATCH 09/11] feat: add retries with exponential backoff to argent API service --- package-lock.json | 571 +++++++---------------------- services/argentPortfolioService.ts | 64 +++- 2 files changed, 200 insertions(+), 435 deletions(-) diff --git a/package-lock.json b/package-lock.json index a0fdbaf2..1d933e92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -96,7 +96,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -821,7 +820,6 @@ "version": "7.25.2", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -830,7 +828,6 @@ "version": "7.25.2", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", - "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -898,7 +895,6 @@ "version": "7.25.2", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", - "dev": true, "dependencies": { "@babel/compat-data": "^7.25.2", "@babel/helper-validator-option": "^7.24.8", @@ -993,7 +989,6 @@ "version": "7.25.2", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", - "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.24.7", "@babel/helper-simple-access": "^7.24.7", @@ -1065,7 +1060,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", - "dev": true, "dependencies": { "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7" @@ -1107,7 +1101,6 @@ "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -1130,7 +1123,6 @@ "version": "7.25.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", - "dev": true, "dependencies": { "@babel/template": "^7.25.0", "@babel/types": "^7.25.0" @@ -2643,7 +2635,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -2658,7 +2649,6 @@ "version": "4.11.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", - "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -2667,7 +2657,6 @@ "version": "0.18.0", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", - "dev": true, "dependencies": { "@eslint/object-schema": "^2.1.4", "debug": "^4.3.1", @@ -2681,7 +2670,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", - "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -2690,7 +2678,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", - "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -2712,14 +2699,12 @@ "node_modules/@eslint/eslintrc/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, "engines": { "node": ">=18" }, @@ -2731,7 +2716,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -2743,7 +2727,6 @@ "version": "9.11.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz", "integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==", - "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -2752,7 +2735,6 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", - "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -2761,7 +2743,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", - "dev": true, "dependencies": { "levn": "^0.4.1" }, @@ -2821,7 +2802,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, "engines": { "node": ">=12.22" }, @@ -2834,7 +2814,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", - "dev": true, "engines": { "node": ">=18.18" }, @@ -3980,126 +3959,6 @@ "node": ">= 10" } }, - "node_modules/@next/swc-darwin-x64": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz", - "integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz", - "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz", - "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz", - "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz", - "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz", - "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz", - "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz", - "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@nimiq/style": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/@nimiq/style/-/style-0.8.5.tgz", @@ -4191,25 +4050,6 @@ "@parcel/watcher-win32-x64": "2.4.1" } }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", - "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@parcel/watcher-darwin-arm64": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", @@ -4229,139 +4069,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", - "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", - "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", - "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", - "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", - "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", - "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", - "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@parcel/watcher-wasm": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-wasm/-/watcher-wasm-2.4.1.tgz", @@ -4387,63 +4094,6 @@ "inBundle": true, "license": "MIT" }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", - "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", - "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", - "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -5582,8 +5232,7 @@ "node_modules/@tweenjs/tween.js": { "version": "18.6.4", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-18.6.4.tgz", - "integrity": "sha512-lB9lMjuqjtuJrx7/kOkqQBtllspPIN+96OvTCeJ2j5FEzinoAXTdAMFnDAQT1KVPRlnYfBrqxtqP66vDM40xxQ==", - "dev": true + "integrity": "sha512-lB9lMjuqjtuJrx7/kOkqQBtllspPIN+96OvTCeJ2j5FEzinoAXTdAMFnDAQT1KVPRlnYfBrqxtqP66vDM40xxQ==" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -5708,8 +5357,7 @@ "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, "node_modules/@types/graceful-fs": { "version": "4.1.9", @@ -5763,8 +5411,7 @@ "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/json5": { "version": "0.0.29", @@ -5846,7 +5493,6 @@ "version": "0.155.1", "resolved": "https://registry.npmjs.org/@types/three/-/three-0.155.1.tgz", "integrity": "sha512-uNUwnz/wWRxahjIqTtDYQ1qdE1R1py21obxfuILkT+kKrjocMwRLQQA1l8nMxfQU7VXb7CXu04ucMo8OqZt4ZA==", - "dev": true, "dependencies": { "@tweenjs/tween.js": "~18.6.4", "@types/stats.js": "*", @@ -6505,7 +6151,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -6538,7 +6183,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -7198,7 +6842,6 @@ "version": "4.23.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", - "dev": true, "funding": [ { "type": "opencollective", @@ -7806,8 +7449,7 @@ "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/cookie-es": { "version": "1.2.2", @@ -8350,8 +7992,7 @@ "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "node_modules/deepmerge": { "version": "4.3.1", @@ -8558,8 +8199,7 @@ "node_modules/electron-to-chromium": { "version": "1.5.9", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.9.tgz", - "integrity": "sha512-HfkT8ndXR0SEkU8gBQQM3rz035bpE/hxkZ1YIt4KJPEFES68HfIU6LzKukH0H794Lm83WJtkSAMfEToxCs15VA==", - "dev": true + "integrity": "sha512-HfkT8ndXR0SEkU8gBQQM3rz035bpE/hxkZ1YIt4KJPEFES68HfIU6LzKukH0H794Lm83WJtkSAMfEToxCs15VA==" }, "node_modules/elliptic": { "version": "6.5.7", @@ -8868,7 +8508,6 @@ "version": "9.11.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz", "integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==", - "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", @@ -9179,7 +8818,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -9194,7 +8832,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -9210,7 +8847,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -9221,14 +8857,12 @@ "node_modules/eslint/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/eslint/node_modules/eslint-scope": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", - "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -9244,7 +8878,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", - "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -9256,7 +8889,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -9272,7 +8904,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -9281,7 +8912,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -9296,7 +8926,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -9311,7 +8940,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -9323,7 +8951,6 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", - "dev": true, "dependencies": { "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", @@ -9340,7 +8967,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", - "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -9364,7 +8990,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -9376,7 +9001,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -9517,14 +9141,12 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "node_modules/fast-redact": { "version": "3.5.0", @@ -9601,7 +9223,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, "dependencies": { "flat-cache": "^4.0.0" }, @@ -9680,7 +9301,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -9692,8 +9312,7 @@ "node_modules/flatted": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, "node_modules/for-each": { "version": "0.3.3", @@ -9814,7 +9433,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -9939,7 +9557,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -10329,7 +9946,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "engines": { "node": ">=0.8.19" } @@ -10683,7 +10299,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -12863,8 +12478,7 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -12874,20 +12488,17 @@ "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -12932,7 +12543,6 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, "dependencies": { "json-buffer": "3.0.1" } @@ -12980,7 +12590,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -13000,8 +12609,7 @@ "node_modules/lil-gui": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.17.0.tgz", - "integrity": "sha512-MVBHmgY+uEbmJNApAaPbtvNh1RCAeMnKym82SBjtp5rODTYKWtM+MXHCifLe2H2Ti1HuBGBtK/5SyG4ShQ3pUQ==", - "dev": true + "integrity": "sha512-MVBHmgY+uEbmJNApAaPbtvNh1RCAeMnKym82SBjtp5rODTYKWtM+MXHCifLe2H2Ti1HuBGBtK/5SyG4ShQ3pUQ==" }, "node_modules/lilconfig": { "version": "2.1.0", @@ -13088,8 +12696,7 @@ "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, "node_modules/loose-envify": { "version": "1.4.0", @@ -13128,7 +12735,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, "dependencies": { "yallist": "^3.0.2" } @@ -13421,8 +13027,7 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "node_modules/natural-compare-lite": { "version": "1.4.0", @@ -13587,8 +13192,7 @@ "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -13799,7 +13403,6 @@ "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -13816,7 +13419,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -13912,7 +13514,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "engines": { "node": ">=8" } @@ -14270,7 +13871,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, "engines": { "node": ">= 0.8.0" } @@ -15712,7 +15312,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "engines": { "node": ">=8" }, @@ -15962,8 +15561,7 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" }, "node_modules/thenify": { "version": "3.3.1", @@ -16316,7 +15914,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -16418,7 +16015,6 @@ "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16614,7 +16210,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -16649,7 +16244,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -17017,7 +16611,6 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -17209,8 +16802,7 @@ "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yaml": { "version": "1.10.2", @@ -17267,7 +16859,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "engines": { "node": ">=10" }, @@ -17298,6 +16889,126 @@ "optional": true } } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz", + "integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz", + "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz", + "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz", + "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz", + "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz", + "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz", + "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz", + "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } -} \ No newline at end of file +} diff --git a/services/argentPortfolioService.ts b/services/argentPortfolioService.ts index ef3925f8..0878f0c9 100644 --- a/services/argentPortfolioService.ts +++ b/services/argentPortfolioService.ts @@ -16,6 +16,11 @@ const API_HEADERS = { "argent-version": "1.4.3", }; +const DEFAULT_MAX_RETRIES = 3; +const DEFAULT_INITIAL_DELAY = 2000; // in milliseconds +const DEFAULT_MAX_DELAY = 10000; // Cap maximum delay at 10 seconds +const DEFAULT_BACKOFF_FACTOR = 2; + const fetchData = async (endpoint: string): Promise => { try { const response = await fetch(endpoint, { headers: API_HEADERS }); @@ -31,23 +36,67 @@ const fetchData = async (endpoint: string): Promise => { } }; +const fetchDataWithRetry = async ( + endpoint: string, + options?: { + maxRetries?: number; + initialDelay?: number; + maxDelay?: number; + backoffFactor?: number; + filterErrorRetryable?: (error: Error) => boolean; + } +): Promise => { + const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES; + const initialDelay = options?.initialDelay ?? DEFAULT_INITIAL_DELAY; + const maxDelay = options?.maxDelay ?? DEFAULT_MAX_DELAY; + const backoffFactor = options?.backoffFactor ?? DEFAULT_BACKOFF_FACTOR; + const filterErrorRetryable = options?.filterErrorRetryable ?? (() => true); + let retries = 0; + let delay = initialDelay; + let lastError: Error = new Error('No request attempted'); + + while (retries <= maxRetries) { + try { + return await fetchData(endpoint); + } catch (err) { + lastError = err instanceof Error ? err : new Error(String(err)); + + if (!filterErrorRetryable(lastError)) { + throw lastError; + } + + if (retries === maxRetries) { + break; + } + + await new Promise((resolve) => setTimeout(resolve, delay)); + delay = Math.min(delay * backoffFactor, maxDelay); // Cap the delay + retries++; + } + } + + throw new Error( + `Failed after ${maxRetries} retries. Endpoint: ${endpoint}. Error: ${lastError.message}` + ); +}; + export const fetchDapps = async () => { - const data = await fetchData(`${API_BASE}/${API_VERSION}/tokens/dapps?chain=starknet`); + const data = await fetchDataWithRetry(`${API_BASE}/${API_VERSION}/tokens/dapps?chain=starknet`); return Object.fromEntries(data.map((dapp) => [dapp.dappId, dapp])) as ArgentDappMap; }; export const fetchTokens = async () => { - const data = await fetchData<{ tokens: ArgentToken[] }>(`${API_BASE}/${API_VERSION}/tokens/info?chain=starknet`); + const data = await fetchDataWithRetry<{ tokens: ArgentToken[] }>(`${API_BASE}/${API_VERSION}/tokens/info?chain=starknet`); return Object.fromEntries(data.tokens.map((token) => [token.address, token])) as ArgentTokenMap; }; export const fetchUserTokens = async (walletAddress: string) => { - const data = await fetchData<{ balances: ArgentUserToken[], status: string }>(`${API_BASE}/${API_VERSION}/activity/starknet/mainnet/account/${walletAddress}/balance`); + const data = await fetchDataWithRetry<{ balances: ArgentUserToken[], status: string }>(`${API_BASE}/${API_VERSION}/activity/starknet/mainnet/account/${walletAddress}/balance`); return data.balances; }; export const fetchUserDapps = async (walletAddress: string) => { - const data = await fetchData<{ dapps: ArgentUserDapp[] }>(`${API_BASE}/${API_VERSION}/tokens/defi/decomposition/${walletAddress}?chain=starknet`); + const data = await fetchDataWithRetry<{ dapps: ArgentUserDapp[] }>(`${API_BASE}/${API_VERSION}/tokens/defi/decomposition/${walletAddress}?chain=starknet`); return data.dapps; }; @@ -58,7 +107,12 @@ export const calculateTokenPrice = async ( ) => { let data: ArgentTokenValue; try { - data = await fetchData(`${API_BASE}/${API_VERSION}/tokens/prices/${tokenAddress}?chain=starknet¤cy=${currency}`); + data = await fetchDataWithRetry( + `${API_BASE}/${API_VERSION}/tokens/prices/${tokenAddress}?chain=starknet¤cy=${currency}`, + { + filterErrorRetryable: (error) => !error.message.includes('No price for token') + } + ); } catch (err) { if (err instanceof Error && err.message.includes('No price for token')) { return 0; From 14db789e30c35ee6ff4a353b07ba7350beab83c2 Mon Sep 17 00:00:00 2001 From: Konstantin Fastov Date: Mon, 4 Nov 2024 22:19:36 +0700 Subject: [PATCH 10/11] feat: add abort controller support to Argent API service --- app/[addressOrDomain]/page.tsx | 30 ++++++++++++++----------- services/argentPortfolioService.ts | 35 +++++++++++++++++++----------- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/app/[addressOrDomain]/page.tsx b/app/[addressOrDomain]/page.tsx index f1a59202..72a5fac3 100644 --- a/app/[addressOrDomain]/page.tsx +++ b/app/[addressOrDomain]/page.tsx @@ -221,10 +221,10 @@ export default function Page({ params }: AddressOrDomainProps) { } try { - const value = await calculateTokenPrice( - token.tokenAddress, - tokenToDecimal(token.tokenBalance, tokenInfo.decimals), - "USD" + const value = await calculateTokenPrice( + token.tokenAddress, + tokenToDecimal(token.tokenBalance, tokenInfo.decimals), + "USD" ); return { value, @@ -369,7 +369,7 @@ export default function Page({ params }: AddressOrDomainProps) { const handleDebt = async ( protocolsMap: ChartItemMap, userDapps: ArgentUserDapp[], - tokens: ArgentTokenMap + tokens: ArgentTokenMap, ) => { const debtStatus = userHasDebt(userDapps); if (!debtStatus || !debtStatus.hasDebt) { @@ -395,7 +395,7 @@ export default function Page({ params }: AddressOrDomainProps) { protocolsMap: ChartItemMap, userTokens: ArgentUserToken[], tokens: ArgentTokenMap, - dapps: ArgentDappMap + dapps: ArgentDappMap, ) => { for await (const token of userTokens) { const tokenInfo = tokens[token.tokenAddress]; @@ -429,7 +429,7 @@ export default function Page({ params }: AddressOrDomainProps) { protocolsMap: ChartItemMap, userDapps: ArgentUserDapp[], tokens: ArgentTokenMap, - dapps: ArgentDappMap + dapps: ArgentDappMap, ) => { for await (const userDapp of userDapps) { if (protocolsMap[userDapp.dappId]) { @@ -530,17 +530,17 @@ export default function Page({ params }: AddressOrDomainProps) { } }, [address]); - const fetchPortfolioData = useCallback(async (addr: string) => { + const fetchPortfolioData = useCallback(async (addr: string, abortController: AbortController) => { setLoadingProtocols(true); try { // Argent API requires lowercase address const normalizedAddr = addr.toLowerCase(); const [dappsData, tokensData, userTokensData, userDappsData] = await Promise.all([ - fetchDapps(), - fetchTokens(), - fetchUserTokens(normalizedAddr), - fetchUserDapps(normalizedAddr), + fetchDapps({ signal: abortController.signal }), + fetchTokens({ signal: abortController.signal }), + fetchUserTokens(normalizedAddr, { signal: abortController.signal }), + fetchUserDapps(normalizedAddr, { signal: abortController.signal }), ]); const data = { @@ -563,10 +563,14 @@ export default function Page({ params }: AddressOrDomainProps) { }, [fetchPortfolioProtocols, fetchPortfolioAssets]); useEffect(() => { + const abortController = new AbortController(); + if (!identity) return; fetchQuestData(identity.owner); fetchPageData(identity.owner); - fetchPortfolioData(identity.owner); + fetchPortfolioData(identity.owner, abortController); + + return () => abortController.abort(); }, [identity]); useEffect(() => setNotFound(false), [dynamicRoute]); diff --git a/services/argentPortfolioService.ts b/services/argentPortfolioService.ts index 0878f0c9..ff7db7a2 100644 --- a/services/argentPortfolioService.ts +++ b/services/argentPortfolioService.ts @@ -21,9 +21,9 @@ const DEFAULT_INITIAL_DELAY = 2000; // in milliseconds const DEFAULT_MAX_DELAY = 10000; // Cap maximum delay at 10 seconds const DEFAULT_BACKOFF_FACTOR = 2; -const fetchData = async (endpoint: string): Promise => { +const fetchData = async (endpoint: string, { signal }: { signal?: AbortSignal }): Promise => { try { - const response = await fetch(endpoint, { headers: API_HEADERS }); + const response = await fetch(endpoint, { headers: API_HEADERS, signal }); if (!response.ok) { throw new Error( `Error ${response.status}: ${await response.text()}` @@ -44,6 +44,7 @@ const fetchDataWithRetry = async ( maxDelay?: number; backoffFactor?: number; filterErrorRetryable?: (error: Error) => boolean; + signal?: AbortSignal; } ): Promise => { const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES; @@ -51,16 +52,22 @@ const fetchDataWithRetry = async ( const maxDelay = options?.maxDelay ?? DEFAULT_MAX_DELAY; const backoffFactor = options?.backoffFactor ?? DEFAULT_BACKOFF_FACTOR; const filterErrorRetryable = options?.filterErrorRetryable ?? (() => true); + const { signal } = options ?? {}; let retries = 0; let delay = initialDelay; let lastError: Error = new Error('No request attempted'); while (retries <= maxRetries) { try { - return await fetchData(endpoint); + return await fetchData(endpoint, { signal }); } catch (err) { lastError = err instanceof Error ? err : new Error(String(err)); + // Prevent abort errors from being retried + if (lastError.name === 'AbortError') { + throw lastError; + } + if (!filterErrorRetryable(lastError)) { throw lastError; } @@ -80,37 +87,39 @@ const fetchDataWithRetry = async ( ); }; -export const fetchDapps = async () => { - const data = await fetchDataWithRetry(`${API_BASE}/${API_VERSION}/tokens/dapps?chain=starknet`); +export const fetchDapps = async ({ signal }: { signal?: AbortSignal }) => { + const data = await fetchDataWithRetry(`${API_BASE}/${API_VERSION}/tokens/dapps?chain=starknet`, { signal }); return Object.fromEntries(data.map((dapp) => [dapp.dappId, dapp])) as ArgentDappMap; }; -export const fetchTokens = async () => { - const data = await fetchDataWithRetry<{ tokens: ArgentToken[] }>(`${API_BASE}/${API_VERSION}/tokens/info?chain=starknet`); +export const fetchTokens = async ({ signal }: { signal?: AbortSignal }) => { + const data = await fetchDataWithRetry<{ tokens: ArgentToken[] }>(`${API_BASE}/${API_VERSION}/tokens/info?chain=starknet`, { signal }); return Object.fromEntries(data.tokens.map((token) => [token.address, token])) as ArgentTokenMap; }; -export const fetchUserTokens = async (walletAddress: string) => { - const data = await fetchDataWithRetry<{ balances: ArgentUserToken[], status: string }>(`${API_BASE}/${API_VERSION}/activity/starknet/mainnet/account/${walletAddress}/balance`); +export const fetchUserTokens = async (walletAddress: string, { signal }: { signal?: AbortSignal }) => { + const data = await fetchDataWithRetry<{ balances: ArgentUserToken[], status: string }>(`${API_BASE}/${API_VERSION}/activity/starknet/mainnet/account/${walletAddress}/balance`, { signal }); return data.balances; }; -export const fetchUserDapps = async (walletAddress: string) => { - const data = await fetchDataWithRetry<{ dapps: ArgentUserDapp[] }>(`${API_BASE}/${API_VERSION}/tokens/defi/decomposition/${walletAddress}?chain=starknet`); +export const fetchUserDapps = async (walletAddress: string, { signal }: { signal?: AbortSignal }) => { + const data = await fetchDataWithRetry<{ dapps: ArgentUserDapp[] }>(`${API_BASE}/${API_VERSION}/tokens/defi/decomposition/${walletAddress}?chain=starknet`, { signal }); return data.dapps; }; export const calculateTokenPrice = async ( tokenAddress: string, tokenAmount: string, - currency: "USD" | "EUR" | "GBP" = "USD" + currency: "USD" | "EUR" | "GBP" = "USD", + { signal }: { signal?: AbortSignal } = {} ) => { let data: ArgentTokenValue; try { data = await fetchDataWithRetry( `${API_BASE}/${API_VERSION}/tokens/prices/${tokenAddress}?chain=starknet¤cy=${currency}`, { - filterErrorRetryable: (error) => !error.message.includes('No price for token') + filterErrorRetryable: (error) => !error.message.includes('No price for token'), + signal } ); } catch (err) { From ac38311b7c7d0401a23800f63d41b9d04c787844 Mon Sep 17 00:00:00 2001 From: Konstantin Fastov Date: Tue, 5 Nov 2024 11:52:09 +0700 Subject: [PATCH 11/11] fix: remove error notification if caused by AbortError --- app/[addressOrDomain]/page.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/[addressOrDomain]/page.tsx b/app/[addressOrDomain]/page.tsx index 72a5fac3..eb4d19b5 100644 --- a/app/[addressOrDomain]/page.tsx +++ b/app/[addressOrDomain]/page.tsx @@ -555,8 +555,14 @@ export default function Page({ params }: AddressOrDomainProps) { fetchPortfolioAssets(data), ]); } catch (error) { - showNotification("Error while fetching address portfolio", "error"); console.log("Error while fetching address portfolio", error); + if (error instanceof Error && error.name === 'AbortError') { + // Do not show notification for AbortError + return; + } + + showNotification("Error while fetching address portfolio", "error"); + } finally { setLoadingProtocols(false); }