From b9eb5b3214c57d047fdffbbd7ccb6e039e9c6af3 Mon Sep 17 00:00:00 2001 From: Alunara Date: Thu, 11 Dec 2025 02:25:35 +0100 Subject: [PATCH 01/11] refactor: add prefetchTokenBalance in addition to useTokenBalance --- .../borrow/hooks/useMaxTokenValues.tsx | 4 +- .../manage-loan/LoanFormTokenInput.tsx | 9 +- .../src/hooks/useTokenBalance.tsx | 65 ------------ .../src/queries/token-balance.query.ts | 98 +++++++++++++++++++ 4 files changed, 107 insertions(+), 69 deletions(-) delete mode 100644 packages/curve-ui-kit/src/hooks/useTokenBalance.tsx create mode 100644 packages/curve-ui-kit/src/queries/token-balance.query.ts diff --git a/apps/main/src/llamalend/features/borrow/hooks/useMaxTokenValues.tsx b/apps/main/src/llamalend/features/borrow/hooks/useMaxTokenValues.tsx index 9d8368a411..14e7841bf2 100644 --- a/apps/main/src/llamalend/features/borrow/hooks/useMaxTokenValues.tsx +++ b/apps/main/src/llamalend/features/borrow/hooks/useMaxTokenValues.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react' import type { UseFormReturn } from 'react-hook-form' import { type Address } from 'viem' -import { useTokenBalance } from '@ui-kit/hooks/useTokenBalance' +import { useTokenBalance } from '@ui-kit/queries/token-balance.query' import { Decimal } from '@ui-kit/utils' import { useCreateLoanMaxReceive } from '../../../queries/create-loan/create-loan-max-receive.query' import { useMarketMaxLeverage } from '../../../queries/market-max-leverage.query' @@ -26,7 +26,7 @@ export function useMaxTokenValues( data: userBalance, error: balanceError, isLoading: isBalanceLoading, - } = useTokenBalance(params, collateralToken) + } = useTokenBalance({ ...params, tokenAddress: collateralToken?.address, tokenSymbol: collateralToken?.symbol }) const { data: maxBorrow, error: maxBorrowError, isLoading: isLoadingMaxBorrow } = useCreateLoanMaxReceive(params) const { data: maxTotalLeverage, diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx index c553f7a154..b6611ac7a4 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx @@ -6,7 +6,7 @@ import { setValueOptions } from '@/llamalend/features/borrow/react-form.utils' import type { LlamaNetwork } from '@/llamalend/llamalend.types' import type { INetworkName } from '@curvefi/llamalend-api/lib/interfaces' import type { PartialRecord } from '@curvefi/prices-api/objects.util' -import { useTokenBalance } from '@ui-kit/hooks/useTokenBalance' +import { useTokenBalance } from '@ui-kit/queries/token-balance.query' import { LargeTokenInput } from '@ui-kit/shared/ui/LargeTokenInput' import { TokenLabel } from '@ui-kit/shared/ui/TokenLabel' import type { Query } from '@ui-kit/types/util' @@ -49,7 +49,12 @@ export const LoanFormTokenInput = < data: balance, isLoading: isBalanceLoading, error: balanceError, - } = useTokenBalance({ chainId: network?.chainId, userAddress }, token) + } = useTokenBalance({ + chainId: network?.chainId, + userAddress, + tokenAddress: token?.address, + tokenSymbol: token?.symbol, + }) const errors = form.formState.errors as PartialRecord, Error> const relatedMaxFieldError = max?.fieldName && errors[max.fieldName] diff --git a/packages/curve-ui-kit/src/hooks/useTokenBalance.tsx b/packages/curve-ui-kit/src/hooks/useTokenBalance.tsx deleted file mode 100644 index 3f1a3d7414..0000000000 --- a/packages/curve-ui-kit/src/hooks/useTokenBalance.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useMemo } from 'react' -import { type Address, erc20Abi, ethAddress, formatUnits, zeroAddress } from 'viem' -import { useConfig } from 'wagmi' -import { useBalance, useReadContracts } from 'wagmi' -import type { FieldsOf } from '@ui-kit/lib' -import type { Query } from '@ui-kit/types/util' -import { Decimal } from '@ui-kit/utils' -import type { GetBalanceReturnType } from '@wagmi/core' - -/** Convert user collateral from GetBalanceReturnType to number */ -const convertBalance = ({ value, decimals }: Partial) => - formatUnits(value || 0n, decimals || 18) as Decimal - -export function useChainConfig(chainId: number | null | undefined) { - const config = useConfig() - return useMemo(() => config.chains.find((chain) => chain.id === chainId), [config.chains, chainId]) -} - -/** - * Hook to fetch the token balance and convert it to a number, wrapping wagmi's useBalance and useReadContracts. - * The useBalance hook is used for native tokens, while useReadContracts is used for ERC-20 tokens. - * @param chainId The ID of the blockchain network. - * @param userAddress The address of the user whose balance is to be fetched. - * @param token The token object containing its address. If the address is the Ethereum address, it fetches the native balance. - * @returns An object containing the balance data (Decimal type), loading state, and error. - */ -export function useTokenBalance( - { chainId, userAddress }: FieldsOf<{ chainId: number; userAddress: Address }>, - token: { address: Address; symbol?: string } | undefined, -): Query { - const nativeCurrencySymbol = useChainConfig(chainId)?.nativeCurrency.symbol - const isNative = token?.address == ethAddress || token?.symbol === nativeCurrencySymbol - const { - data: nativeBalanceData, - error: nativeBalanceError, - isLoading: nativeBalanceLoading, - } = useBalance({ - ...(isNative && userAddress && chainId && { address: userAddress, chainId }), - }) - - const tokenAddress = isNative ? undefined : token?.address - const { - data: tokenBalanceData, - error: tokenBalanceError, - isLoading: tokenBalanceLoading, - } = useReadContracts({ - allowFailure: false, - contracts: [ - { address: tokenAddress, abi: erc20Abi, functionName: 'balanceOf', args: [userAddress ?? zeroAddress] }, - { address: tokenAddress, abi: erc20Abi, functionName: 'decimals' }, - ], - }) - - return isNative - ? { - data: nativeBalanceData && convertBalance(nativeBalanceData), - error: nativeBalanceError, - isLoading: nativeBalanceLoading, - } - : { - data: tokenBalanceData && convertBalance({ value: tokenBalanceData[0], decimals: tokenBalanceData[1] }), - error: tokenBalanceError, - isLoading: tokenBalanceLoading, - } -} diff --git a/packages/curve-ui-kit/src/queries/token-balance.query.ts b/packages/curve-ui-kit/src/queries/token-balance.query.ts new file mode 100644 index 0000000000..ca8d54aa32 --- /dev/null +++ b/packages/curve-ui-kit/src/queries/token-balance.query.ts @@ -0,0 +1,98 @@ +import { erc20Abi, ethAddress, formatUnits, type Address } from 'viem' +import { useConfig } from 'wagmi' +import { useBalance, useReadContracts } from 'wagmi' +import type { FieldsOf } from '@ui-kit/lib' +import { queryClient } from '@ui-kit/lib/api' +import type { ChainQuery, UserQuery } from '@ui-kit/lib/model' +import { Decimal } from '@ui-kit/utils' +import type { Config } from '@wagmi/core' +import type { GetBalanceReturnType } from '@wagmi/core' +import { getBalanceQueryOptions, readContractsQueryOptions } from '@wagmi/core/query' + +type TokenQuery = { tokenAddress: Address } +type TokenSymbolQuery = { tokenSymbol: string } + +/** Convert user collateral from GetBalanceReturnType to number */ +const convertBalance = ({ value, decimals }: Partial) => + formatUnits(value || 0n, decimals || 18) as Decimal + +/** Create query options for native token balance */ +const getNativeBalanceQueryOptions = (config: Config, { chainId, userAddress }: ChainQuery & UserQuery) => + getBalanceQueryOptions(config, { + chainId, + address: userAddress, + }) + +/** Create query contracts for ERC-20 token balance and decimals */ +const getERC20QueryContracts = ({ chainId, userAddress, tokenAddress }: ChainQuery & UserQuery & TokenQuery) => + [ + { chainId, address: tokenAddress, abi: erc20Abi, functionName: 'balanceOf', args: [userAddress] }, + { chainId, address: tokenAddress, abi: erc20Abi, functionName: 'decimals' }, + ] as const + +/** Function that gets the native currency symbol for a given chain */ +const getNativeCurrencySymbol = (config: Config, chainId: number) => + config.chains.find((chain) => chain.id === chainId)?.nativeCurrency?.symbol + +/** Function that checks if a given token address and symbol are the chain's native currency symbol */ +const isNative = (config: Config, { chainId, tokenAddress, tokenSymbol }: ChainQuery & TokenQuery & TokenSymbolQuery) => + tokenAddress === ethAddress || tokenSymbol === getNativeCurrencySymbol(config, chainId) + +/** Imperatively fetch token balance */ +export const fetchTokenBalance = async ( + config: Config, + query: ChainQuery & UserQuery & TokenQuery & TokenSymbolQuery, +) => + isNative(config, query) + ? await queryClient + .fetchQuery(getNativeBalanceQueryOptions(config, query)) + .then((balance) => convertBalance({ value: balance.value, decimals: balance.decimals })) + : await queryClient + .fetchQuery( + readContractsQueryOptions(config, { + allowFailure: false, + contracts: getERC20QueryContracts(query), + }), + ) + .then((balance) => convertBalance({ value: balance[0], decimals: balance[1] })) + +/** Hook to fetch the token balance */ +export function useTokenBalance({ + chainId, + userAddress, + tokenAddress, + tokenSymbol, +}: FieldsOf) { + const config = useConfig() + + const isEnabled = chainId != null && userAddress != null && tokenAddress != null && tokenSymbol != null + const isNativeToken = isEnabled && isNative(config, { chainId, tokenAddress, tokenSymbol }) + + const nativeBalance = useBalance({ + ...(isEnabled ? getNativeBalanceQueryOptions(config, { chainId, userAddress }) : {}), + query: { enabled: isEnabled && isNativeToken }, + }) + + const erc20Balance = useReadContracts({ + contracts: isEnabled ? getERC20QueryContracts({ chainId, userAddress, tokenAddress }) : undefined, + query: { enabled: isEnabled && !isNativeToken }, + }) + + return isNativeToken + ? { + data: nativeBalance.data && convertBalance(nativeBalance.data), + error: nativeBalance.error, + isLoading: nativeBalance.isLoading, + } + : { + data: + erc20Balance.data && erc20Balance.data[0].status === 'success' && erc20Balance.data[1].status === 'success' + ? convertBalance({ + value: erc20Balance.data[0].result, + decimals: erc20Balance.data[1].result, + }) + : undefined, + error: erc20Balance.error, + isLoading: erc20Balance.isLoading, + } +} From bfb602d4ca5d29ef40aebd32e2001248bdc745ca Mon Sep 17 00:00:00 2001 From: Alunara Date: Thu, 11 Dec 2025 03:21:28 +0100 Subject: [PATCH 02/11] refactor: use new fetchTokenBalance in createUserBalancesSlice --- apps/main/src/dao/store/createAppSlice.ts | 5 ++- .../ConfirmModal/CreatePoolButton.tsx | 4 +- .../main/src/dex/components/PagePool/Page.tsx | 6 ++- .../dex/components/PageRouterSwap/index.tsx | 25 ++++++++---- apps/main/src/dex/hooks/useAutoRefresh.tsx | 6 ++- apps/main/src/dex/lib/curvejs.ts | 38 ------------------ .../src/dex/store/createCreatePoolSlice.ts | 15 +++---- apps/main/src/dex/store/createGlobalSlice.ts | 12 ++++-- apps/main/src/dex/store/createPoolsSlice.ts | 12 +++--- .../src/dex/store/createQuickSwapSlice.ts | 39 ++++++++++++------- .../src/dex/store/createUserBalancesSlice.ts | 29 ++++++++++---- apps/main/src/lend/store/createAppSlice.ts | 5 ++- apps/main/src/loan/store/createAppSlice.ts | 5 ++- .../connect-wallet/lib/CurveProvider.tsx | 6 ++- .../src/features/connect-wallet/lib/types.ts | 2 + 15 files changed, 115 insertions(+), 94 deletions(-) diff --git a/apps/main/src/dao/store/createAppSlice.ts b/apps/main/src/dao/store/createAppSlice.ts index ec2d03d764..9e8e663609 100644 --- a/apps/main/src/dao/store/createAppSlice.ts +++ b/apps/main/src/dao/store/createAppSlice.ts @@ -1,5 +1,6 @@ import { produce } from 'immer' import lodash from 'lodash' +import type { Config } from 'wagmi' import type { StoreApi } from 'zustand' import type { State } from '@/dao/store/useStore' import type { CurveApi, Wallet } from '@/dao/types/dao.types' @@ -15,7 +16,7 @@ export interface AppSlice extends SliceState { updateGlobalStoreByKey: (key: DefaultStateKeys, value: T) => void /** Hydrate resets states and refreshes store data from the API */ - hydrate(api: CurveApi | undefined, prevApi: CurveApi | undefined, wallet: Wallet | undefined): Promise + hydrate(config: Config, api: CurveApi | undefined, prevApi: CurveApi | undefined, wallet: Wallet | undefined): Promise setAppStateByActiveKey(sliceKey: SliceKey, key: StateKey, activeKey: string, value: T): void setAppStateByKey(sliceKey: SliceKey, key: StateKey, value: T): void @@ -35,7 +36,7 @@ const createAppSlice = (set: StoreApi['setState'], get: StoreApi[' ) }, - hydrate: async (api, prevApi, wallet) => { + hydrate: async (config, api, prevApi, wallet) => { if (!api) return const isNetworkSwitched = prevApi?.chainId != api.chainId diff --git a/apps/main/src/dex/components/PageCreatePool/ConfirmModal/CreatePoolButton.tsx b/apps/main/src/dex/components/PageCreatePool/ConfirmModal/CreatePoolButton.tsx index 507bf3c0af..8f7d3a1d9b 100644 --- a/apps/main/src/dex/components/PageCreatePool/ConfirmModal/CreatePoolButton.tsx +++ b/apps/main/src/dex/components/PageCreatePool/ConfirmModal/CreatePoolButton.tsx @@ -1,4 +1,5 @@ import { styled } from 'styled-components' +import { useConfig } from 'wagmi' import InfoLinkBar from '@/dex/components/PageCreatePool/ConfirmModal/CreateInfoLinkBar' import { useNetworks } from '@/dex/entities/networks' import { curveProps } from '@/dex/lib/utils' @@ -25,6 +26,7 @@ const CreatePoolButton = ({ disabled, curve }: Props) => { const poolId = useStore((state) => state.createPool.transactionState.poolId) const errorMessage = useStore((state) => state.createPool.transactionState.errorMessage) const { connectState, connect: connectWallet } = useWallet() + const config = useConfig() return !haveSigner ? ( connectWallet()} loading={isLoading(connectState)}> @@ -39,7 +41,7 @@ const CreatePoolButton = ({ disabled, curve }: Props) => { )} {(txStatus === '' || txStatus === 'ERROR') && ( - deployPool(curve)}> + deployPool(config, curve)}> {t`Create Pool`} )} diff --git a/apps/main/src/dex/components/PagePool/Page.tsx b/apps/main/src/dex/components/PagePool/Page.tsx index 763e389051..54a359f536 100644 --- a/apps/main/src/dex/components/PagePool/Page.tsx +++ b/apps/main/src/dex/components/PagePool/Page.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react' +import { useConfig } from 'wagmi' import Transfer from '@/dex/components/PagePool/index' import { ROUTE } from '@/dex/constants' import { useNetworkByChain } from '@/dex/entities/networks' @@ -30,12 +31,13 @@ export const PagePool = () => { const poolDataCacheOrApi = useMemo(() => poolData || poolDataCache, [poolData, poolDataCache]) + const config = useConfig() useEffect(() => { if (!rChainId || !poolId || curveApi?.chainId !== rChainId || !haveAllPools || poolData) return - fetchNewPool(curveApi, poolId) + fetchNewPool(config, curveApi, poolId) .then((found) => setPoolNotFound(!found)) .catch(() => setPoolNotFound(true)) - }, [curveApi, fetchNewPool, haveAllPools, network, poolId, poolData, push, rChainId]) + }, [config, curveApi, fetchNewPool, haveAllPools, network, poolId, poolData, push, rChainId]) return !rFormType || network.excludePoolsMapper[poolId ?? ''] || poolNotFound ? ( diff --git a/apps/main/src/dex/components/PageRouterSwap/index.tsx b/apps/main/src/dex/components/PageRouterSwap/index.tsx index dd20c927e2..5eca30bc4c 100644 --- a/apps/main/src/dex/components/PageRouterSwap/index.tsx +++ b/apps/main/src/dex/components/PageRouterSwap/index.tsx @@ -1,6 +1,7 @@ import lodash from 'lodash' import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ethAddress } from 'viem' +import { useConfig } from 'wagmi' import ChipInpHelper from '@/dex/components/ChipInpHelper' import DetailInfoEstGas from '@/dex/components/DetailInfoEstGas' import FieldHelperUsdRate from '@/dex/components/FieldHelperUsdRate' @@ -131,6 +132,7 @@ const QuickSwap = ({ const fromToken = tokens.find((x) => x.address.toLocaleLowerCase() == fromAddress) const toToken = tokens.find((x) => x.address.toLocaleLowerCase() == toAddress) + const config = useConfig() const updateFormValues = useCallback( ( updatedFormValues: Partial, @@ -143,6 +145,7 @@ const QuickSwap = ({ setConfirmedLoss(false) void setFormValues( + config, pageLoaded ? curve : null, updatedFormValues, searchedParams, @@ -152,7 +155,7 @@ const QuickSwap = ({ isRefetch, ) }, - [curve, storeMaxSlippage, pageLoaded, searchedParams, setFormValues], + [config, curve, storeMaxSlippage, pageLoaded, searchedParams, setFormValues], ) const handleBtnClickSwap = useCallback( @@ -175,7 +178,7 @@ const QuickSwap = ({ const { dismiss } = notify(`Please confirm ${notifyMessage}`, 'pending') setTxInfoBar(Pending {notifyMessage}) - const resp = await fetchStepSwap(actionActiveKey, curve, formValues, searchedParams, maxSlippage) + const resp = await fetchStepSwap(actionActiveKey, config, curve, formValues, searchedParams, maxSlippage) if (isSubscribed.current && resp && resp.hash && resp.activeKey === activeKey && !resp.error && network) { const txMessage = t`Transaction complete. Received ${resp.swappedAmount} ${toSymbol}.` @@ -190,7 +193,7 @@ const QuickSwap = ({ if (resp?.error) setTxInfoBar(null) if (typeof dismiss === 'function') dismiss() }, - [activeKey, fetchStepSwap, updateFormValues, network], + [activeKey, config, fetchStepSwap, updateFormValues, network], ) const getSteps = useCallback( @@ -222,7 +225,7 @@ const QuickSwap = ({ onClick: async () => { const notifyMessage = t`Please approve spending your ${fromSymbol}.` const { dismiss } = notify(notifyMessage, 'pending') - await fetchStepApprove(activeKey, curve, formValues, searchedParams, storeMaxSlippage) + await fetchStepApprove(activeKey, config, curve, formValues, searchedParams, storeMaxSlippage) if (typeof dismiss === 'function') dismiss() }, }, @@ -300,7 +303,15 @@ const QuickSwap = ({ return stepsKey.map((key) => stepsObj[key]) }, - [confirmedLoss, fetchStepApprove, storeMaxSlippage, handleBtnClickSwap, slippageImpact?.isExpectedToAmount, steps], + [ + config, + confirmedLoss, + fetchStepApprove, + storeMaxSlippage, + handleBtnClickSwap, + slippageImpact?.isExpectedToAmount, + steps, + ], ) const fetchData = useCallback(() => { @@ -343,9 +354,9 @@ const QuickSwap = ({ useEffect(() => fetchData(), [tokensMapperStr, searchedParams.fromAddress, searchedParams.toAddress]) useEffect(() => { - void updateTokenList(isReady ? curve : null, tokensMapper) + void updateTokenList(config, isReady ? curve : null, tokensMapper) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isReady, tokensMapperStr, curve?.signerAddress]) + }, [config, isReady, tokensMapperStr, curve?.signerAddress]) // re-fetch data usePageVisibleInterval(fetchData, REFRESH_INTERVAL['15s']) diff --git a/apps/main/src/dex/hooks/useAutoRefresh.tsx b/apps/main/src/dex/hooks/useAutoRefresh.tsx index c36f382d86..4cd42236f8 100644 --- a/apps/main/src/dex/hooks/useAutoRefresh.tsx +++ b/apps/main/src/dex/hooks/useAutoRefresh.tsx @@ -1,4 +1,5 @@ import { useCallback, useMemo } from 'react' +import { useConfig } from 'wagmi' import curvejsApi from '@/dex/lib/curvejs' import useStore from '@/dex/store/useStore' import { type CurveApi, useCurve } from '@ui-kit/features/connect-wallet' @@ -32,12 +33,13 @@ export const useAutoRefresh = (chainId: number | undefined) => { [chainId, fetchPoolsTvl, fetchPoolsVolume, poolDataMapper, setTokensMapper], ) + const config = useConfig() usePageVisibleInterval(() => { if (curveApi) { void fetchPoolsVolumeTvl(curveApi) if (curveApi.signerAddress) { - void fetchAllStoredBalances(curveApi) + void fetchAllStoredBalances(config, curveApi) } } }, REFRESH_INTERVAL['5m']) @@ -45,6 +47,6 @@ export const useAutoRefresh = (chainId: number | undefined) => { usePageVisibleInterval(async () => { if (!curveApi || !network) return console.warn('Curve API or network is not defined, cannot refetch pools') const poolIds = await curvejsApi.network.fetchAllPoolsList(curveApi, network) - void fetchPools(curveApi, poolIds, null) + void fetchPools(config, curveApi, poolIds, null) }, REFRESH_INTERVAL['11m']) } diff --git a/apps/main/src/dex/lib/curvejs.ts b/apps/main/src/dex/lib/curvejs.ts index 39fe11d27d..1fc9c9b898 100644 --- a/apps/main/src/dex/lib/curvejs.ts +++ b/apps/main/src/dex/lib/curvejs.ts @@ -17,7 +17,6 @@ import { RewardCrv, RewardOther, RewardsApy, - UserBalancesMapper, } from '@/dex/types/main.types' import { fulfilledValue, getErrorMessage, isValidAddress } from '@/dex/utils' import { @@ -36,7 +35,6 @@ import { } from '@/dex/utils/utilsSwap' import type { IProfit } from '@curvefi/api/lib/interfaces' import type { DateValue } from '@internationalized/date' -import PromisePool from '@supercharge/promise-pool/dist' import { BN } from '@ui/utils' import dayjs from '@ui-kit/lib/dayjs' import { waitForTransaction, waitForTransactions } from '@ui-kit/lib/ethers' @@ -1368,42 +1366,6 @@ const wallet = { log('userPoolShare', p.name) return p.userShare() }, - fetchUserBalances: async (curve: CurveApi, tokenAddresses: string[]) => { - const { chainId } = curve - log('fetchWalletTokensBalances', chainId, tokenAddresses.length) - - const results: UserBalancesMapper = {} - const errors: string[][] = [] - const chunks = chunk(tokenAddresses, 20) - await PromisePool.for(chunks) - .withConcurrency(10) - .handleError((_, chunk) => { - errors.push(chunk) - }) - .process(async (addresses) => { - const balances = (await curve.getBalances(addresses)) as string[] - for (const idx in balances) { - const balance = balances[idx] - const tokenAddress = addresses[idx] - results[tokenAddress] = balance - } - }) - - const fattenErrors = flatten(errors) - - if (fattenErrors.length) { - await PromisePool.for(fattenErrors) - .handleError((error, tokenAddress) => { - console.error(`Unable to get user balance for ${tokenAddress}`, error) - results[tokenAddress] = 'NaN' - }) - .process(async (tokenAddress) => { - const [balance] = (await curve.getBalances([tokenAddress])) as string[] - results[tokenAddress] = balance - }) - } - return results - }, } const lockCrv = { diff --git a/apps/main/src/dex/store/createCreatePoolSlice.ts b/apps/main/src/dex/store/createCreatePoolSlice.ts index d182cf8f09..a8e803eb0c 100644 --- a/apps/main/src/dex/store/createCreatePoolSlice.ts +++ b/apps/main/src/dex/store/createCreatePoolSlice.ts @@ -2,6 +2,7 @@ import { BigNumber } from 'bignumber.js' import type { ContractTransactionResponse } from 'ethers' import { produce } from 'immer' import { zeroAddress } from 'viem' +import type { Config } from 'wagmi' import type { StoreApi } from 'zustand' import { CRYPTOSWAP, @@ -138,7 +139,7 @@ export type CreatePoolSlice = { updateTokensInPoolValidation: (tokensInPool: boolean) => void updateParametersValidation: (parameters: boolean) => void updatePoolInfoValidation: (poolInfo: boolean) => void - deployPool: (curve: CurveApi) => void + deployPool: (config: Config, curve: CurveApi) => void resetState: () => void } } @@ -747,7 +748,7 @@ const createCreatePoolSlice = ( }), ) }, - deployPool: async (curve: CurveApi) => { + deployPool: async (config: Config, curve: CurveApi) => { const chainId = curve.chainId const { pools: { fetchNewPool, basePools }, @@ -848,7 +849,7 @@ const createCreatePoolSlice = ( }), ) - const poolData = await fetchNewPool(curve, poolId) + const poolData = await fetchNewPool(config, curve, poolId) if (poolData) { set( produce((state) => { @@ -925,7 +926,7 @@ const createCreatePoolSlice = ( }), ) - const poolData = await fetchNewPool(curve, poolId) + const poolData = await fetchNewPool(config, curve, poolId) if (poolData) { set( produce((state) => { @@ -1006,7 +1007,7 @@ const createCreatePoolSlice = ( }), ) - const poolData = await fetchNewPool(curve, poolId) + const poolData = await fetchNewPool(config, curve, poolId) if (poolData) { set( produce((state) => { @@ -1103,7 +1104,7 @@ const createCreatePoolSlice = ( }), ) - const poolData = await fetchNewPool(curve, poolId) + const poolData = await fetchNewPool(config, curve, poolId) if (poolData) { set( produce((state) => { @@ -1190,7 +1191,7 @@ const createCreatePoolSlice = ( }), ) - const poolData = await fetchNewPool(curve, poolId) + const poolData = await fetchNewPool(config, curve, poolId) if (poolData) { set( produce((state) => { diff --git a/apps/main/src/dex/store/createGlobalSlice.ts b/apps/main/src/dex/store/createGlobalSlice.ts index 589adb7a48..340522ca49 100644 --- a/apps/main/src/dex/store/createGlobalSlice.ts +++ b/apps/main/src/dex/store/createGlobalSlice.ts @@ -1,5 +1,6 @@ import { produce } from 'immer' import lodash from 'lodash' +import type { Config } from 'wagmi' import type { StoreApi } from 'zustand' import curvejsApi from '@/dex/lib/curvejs' import type { State } from '@/dex/store/useStore' @@ -22,7 +23,12 @@ export interface GlobalSlice extends GlobalState { setNetworkConfigFromApi(curve: CurveApi): void /** Hydrate resets states and refreshes store data from the API */ - hydrate(curveApi: CurveApi | undefined, prevCurveApi: CurveApi | undefined, wallet: Wallet | undefined): Promise + hydrate( + config: Config, + curveApi: CurveApi | undefined, + prevCurveApi: CurveApi | undefined, + wallet: Wallet | undefined, + ): Promise updateGlobalStoreByKey: (key: DefaultStateKeys, value: T) => void @@ -63,7 +69,7 @@ const createGlobalSlice = (set: StoreApi['setState'], get: StoreApi { + hydrate: async (config, curveApi, prevCurveApi) => { if (!curveApi) return const state = get() @@ -117,7 +123,7 @@ const createGlobalSlice = (set: StoreApi['setState'], get: StoreApi Promise fetchPoolsVolume: (chainId: ChainId, poolDatas: PoolData[]) => Promise fetchPools( + config: Config, curve: CurveApi, poolIds: string[], failedFetching24hOldVprice: { [poolAddress: string]: boolean } | null, ): Promise<{ poolsMapper: PoolDataMapper; poolDatas: PoolData[] } | undefined> - fetchNewPool(curve: CurveApi, poolId: string): Promise + fetchNewPool(config: Config, curve: CurveApi, poolId: string): Promise fetchBasePools(curve: CurveApi): Promise fetchPoolsRewardsApy(chainId: ChainId, poolDatas: PoolData[]): Promise fetchMissingPoolsRewardsApy(chainId: ChainId, poolDatas: PoolData[]): Promise @@ -215,7 +217,7 @@ const createPoolsSlice = (set: StoreApi['setState'], get: StoreApi // update cache storeCache.setTvlVolumeMapper('volumeMapper', chainId, volumeMapper) }, - fetchPools: async (curve, poolIds, failedFetching24hOldVprice) => { + fetchPools: async (config, curve, poolIds, failedFetching24hOldVprice) => { const { pools, storeCache, @@ -280,7 +282,7 @@ const createPoolsSlice = (set: StoreApi['setState'], get: StoreApi const partialTokens = await tokens.setTokensMapper(curve, partialPoolDatas) if (curve.signerAddress) { - void userBalances.fetchUserBalancesByTokens(curve, partialTokens) + void userBalances.fetchUserBalancesByTokens(config, curve, partialTokens) } return { poolsMapper, poolDatas: partialPoolDatas } @@ -295,7 +297,7 @@ const createPoolsSlice = (set: StoreApi['setState'], get: StoreApi ) } }, - fetchNewPool: async (curve, poolId) => { + fetchNewPool: async (config, curve, poolId) => { await Promise.allSettled([ curve.factory.fetchNewPools(), curve.cryptoFactory.fetchNewPools(), @@ -303,7 +305,7 @@ const createPoolsSlice = (set: StoreApi['setState'], get: StoreApi curve.tricryptoFactory.fetchNewPools(), curve.stableNgFactory.fetchNewPools(), ]) - const resp = await get()[sliceKey].fetchPools(curve, [poolId], null) + const resp = await get()[sliceKey].fetchPools(config, curve, [poolId], null) return resp?.poolsMapper?.[poolId] }, fetchBasePools: async (curve: CurveApi) => { diff --git a/apps/main/src/dex/store/createQuickSwapSlice.ts b/apps/main/src/dex/store/createQuickSwapSlice.ts index 50dbcd4e88..4d641c2dcb 100644 --- a/apps/main/src/dex/store/createQuickSwapSlice.ts +++ b/apps/main/src/dex/store/createQuickSwapSlice.ts @@ -1,5 +1,6 @@ import lodash from 'lodash' import { ethAddress } from 'viem' +import type { Config } from 'wagmi' import type { StoreApi } from 'zustand' import type { FormEstGas, @@ -39,15 +40,22 @@ const sliceKey = 'quickSwap' export type QuickSwapSlice = { [sliceKey]: SliceState & { fetchUserBalances( + config: Config, curve: CurveApi, fromAddress: string, toAddress: string, ): Promise<{ fromAmount: string; toAmount: string }> fetchMaxAmount(curve: CurveApi, searchedParams: SearchedParams, maxSlippage: string | undefined): Promise - fetchRoutesAndOutput(curve: CurveApi, searchedParams: SearchedParams, maxSlippage: string): Promise + fetchRoutesAndOutput( + config: Config, + curve: CurveApi, + searchedParams: SearchedParams, + maxSlippage: string, + ): Promise fetchEstGasApproval(curve: CurveApi, searchedParams: SearchedParams): Promise resetFormErrors(): void setFormValues( + config: Config, curve: CurveApi | null, updatedFormValues: Partial, searchedParams: SearchedParams, @@ -59,11 +67,12 @@ export type QuickSwapSlice = { // select token list setPoolListFormValues(hideSmallPools: boolean): void - updateTokenList(curve: CurveApi | null, tokensMapper: TokensMapper): Promise + updateTokenList(config: Config, curve: CurveApi | null, tokensMapper: TokensMapper): Promise // steps fetchStepApprove( activeKey: string, + config: Config, curve: CurveApi, formValues: FormValues, searchedParams: SearchedParams, @@ -71,6 +80,7 @@ export type QuickSwapSlice = { ): Promise fetchStepSwap( activeKey: string, + config: Config, curve: CurveApi, formValues: FormValues, searchedParams: SearchedParams, @@ -98,14 +108,14 @@ const createQuickSwapSlice = (set: StoreApi['setState'], get: StoreApi { + fetchUserBalances: async (config, curve, fromAddress, toAddress) => { const { userBalancesMapper, fetchUserBalancesByTokens } = get().userBalances const fetchTokensList = [] if (fromAddress && typeof userBalancesMapper[fromAddress] === 'undefined') fetchTokensList.push(fromAddress) if (toAddress && typeof userBalancesMapper[toAddress] === 'undefined') fetchTokensList.push(toAddress) - if (fetchTokensList.length > 0) await fetchUserBalancesByTokens(curve, fetchTokensList) + if (fetchTokensList.length > 0) await fetchUserBalancesByTokens(config, curve, fetchTokensList) return { fromAmount: get().userBalances.userBalancesMapper[fromAddress] ?? '0', @@ -160,7 +170,7 @@ const createQuickSwapSlice = (set: StoreApi['setState'], get: StoreApi { + fetchRoutesAndOutput: async (config, curve, searchedParams, maxSlippage) => { const state = get() const sliceState = state[sliceKey] @@ -236,7 +246,7 @@ const createQuickSwapSlice = (set: StoreApi['setState'], get: StoreApi +fromAmount ? 'too-much' : '' get()[sliceKey].setStateByKey('formValues', cFormValues) } @@ -280,6 +290,7 @@ const createQuickSwapSlice = (set: StoreApi['setState'], get: StoreApi['setState'], get: StoreApi['setState'], get: StoreApi { + updateTokenList: async (config, curve, tokensMapper) => { const state = get() const sliceState = state[sliceKey] @@ -359,11 +370,11 @@ const createQuickSwapSlice = (set: StoreApi['setState'], get: StoreApi { + fetchStepApprove: async (activeKey, config, curve, formValues, searchedParams, globalMaxSlippage) => { const state = get() const sliceState = state[sliceKey] @@ -397,14 +408,14 @@ const createQuickSwapSlice = (set: StoreApi['setState'], get: StoreApi { + fetchStepSwap: async (activeKey, config, curve, formValues, searchedParams, maxSlippage) => { const state = get() const sliceState = state[sliceKey] @@ -460,7 +471,7 @@ const createQuickSwapSlice = (set: StoreApi['setState'], get: StoreApi - fetchAllStoredBalances(curve: CurveApi): Promise + fetchUserBalancesByTokens(config: Config, curve: CurveApi, addresses: string[]): Promise + fetchAllStoredBalances(config: Config, curve: CurveApi): Promise updateUserBalancesFromPool(tokens: UserBalancesMapper): void setStateByActiveKey(key: StateKey, activeKey: string, value: T): void @@ -41,7 +43,7 @@ const createUserBalancesSlice = ( [sliceKey]: { ...DEFAULT_STATE, - fetchUserBalancesByTokens: async (curve, tokensAddresses) => { + fetchUserBalancesByTokens: async (config, curve, tokensAddresses) => { const state = get() const sliceState = state[sliceKey] @@ -57,7 +59,20 @@ const createUserBalancesSlice = ( const filteredBadTokens = tokensAddresses.filter((address) => !excludeTokensBalancesMapper[address]) sliceState.setStateByKey('loading', true) - const userBalancesMapper = await curvejsApi.wallet.fetchUserBalances(curve, filteredBadTokens) + + // This gets multicall batched by Wagmi and Viem internally + const balances = await Promise.all( + filteredBadTokens.map((token) => + fetchTokenBalance(config, { + chainId, + userAddress: signerAddress, + tokenAddress: token as Address, + tokenSymbol: '', + }).then((balance) => [token, balance]), + ), + ) + + const userBalancesMapper = Object.fromEntries(balances) sliceState.setStateByKeys({ userBalancesMapper: { ...storedUserBalancesMapper, ...userBalancesMapper }, loading: false, @@ -65,9 +80,9 @@ const createUserBalancesSlice = ( return get()[sliceKey].userBalancesMapper }, - fetchAllStoredBalances: async (curve) => { + fetchAllStoredBalances: async (config, curve) => { const tokenAddresses = Object.keys(get().userBalances.userBalancesMapper) - await get().userBalances.fetchUserBalancesByTokens(curve, tokenAddresses) + await get().userBalances.fetchUserBalancesByTokens(config, curve, tokenAddresses) }, updateUserBalancesFromPool: ({ gauge, lpToken, ...rest }) => { get().userBalances.setStateByKey('loading', true) diff --git a/apps/main/src/lend/store/createAppSlice.ts b/apps/main/src/lend/store/createAppSlice.ts index 1857c54bbb..8c98bc81f8 100644 --- a/apps/main/src/lend/store/createAppSlice.ts +++ b/apps/main/src/lend/store/createAppSlice.ts @@ -1,5 +1,6 @@ import { produce } from 'immer' import lodash from 'lodash' +import type { Config } from 'wagmi' import { StoreApi } from 'zustand' import { prefetchMarkets } from '@/lend/entities/chain/chain-query' import type { State } from '@/lend/store/useStore' @@ -12,7 +13,7 @@ export type StateKey = string // prettier-ignore export interface AppSlice { /** Hydrate resets states and refreshes store data from the API */ - hydrate(api: Api | undefined, prevApi: Api | undefined, wallet: Wallet | undefined): Promise + hydrate(config: Config, api: Api | undefined, prevApi: Api | undefined, wallet: Wallet | undefined): Promise setAppStateByActiveKey(sliceKey: SliceKey, key: StateKey, activeKey: string, value: T, showLog?: boolean): void setAppStateByKey(sliceKey: SliceKey, key: StateKey, value: T, showLog?: boolean): void @@ -21,7 +22,7 @@ export interface AppSlice { } const createAppSlice = (set: StoreApi['setState'], get: StoreApi['getState']): AppSlice => ({ - hydrate: async (api, prevApi) => { + hydrate: async (config, api, prevApi) => { if (!api) return const isNetworkSwitched = !!prevApi?.chainId && prevApi.chainId !== api.chainId diff --git a/apps/main/src/loan/store/createAppSlice.ts b/apps/main/src/loan/store/createAppSlice.ts index 31680b5a9c..8227668f33 100644 --- a/apps/main/src/loan/store/createAppSlice.ts +++ b/apps/main/src/loan/store/createAppSlice.ts @@ -1,5 +1,6 @@ import { produce } from 'immer' import lodash from 'lodash' +import type { Config } from 'wagmi' import type { StoreApi } from 'zustand' import { type State } from '@/loan/store/useStore' import { type LlamaApi, Wallet } from '@/loan/types/loan.types' @@ -18,7 +19,7 @@ export interface AppSlice extends SliceState { updateGlobalStoreByKey(key: DefaultStateKeys, value: T): void /** Hydrate resets states and refreshes store data from the API */ - hydrate(curve: LlamaApi | undefined, prevCurveApi: LlamaApi | undefined, wallet: Wallet | undefined): Promise + hydrate(config: Config, curve: LlamaApi | undefined, prevCurveApi: LlamaApi | undefined, wallet: Wallet | undefined): Promise setAppStateByActiveKey(sliceKey: SliceKey, key: StateKey, activeKey: string, value: T, showLog?: boolean): void setAppStateByKey(sliceKey: SliceKey, key: StateKey, value: T, showLog?: boolean): void @@ -41,7 +42,7 @@ const createAppSlice = (set: StoreApi['setState'], get: StoreApi[' ) }, - hydrate: async (curveApi, prevCurveApi) => { + hydrate: async (config, curveApi, prevCurveApi) => { if (!curveApi) return const { loans } = get() diff --git a/packages/curve-ui-kit/src/features/connect-wallet/lib/CurveProvider.tsx b/packages/curve-ui-kit/src/features/connect-wallet/lib/CurveProvider.tsx index afdc508c5e..012e358b2e 100644 --- a/packages/curve-ui-kit/src/features/connect-wallet/lib/CurveProvider.tsx +++ b/packages/curve-ui-kit/src/features/connect-wallet/lib/CurveProvider.tsx @@ -1,5 +1,6 @@ import { type ReactNode, useEffect, useState } from 'react' import { useChainId, useSwitchChain } from 'wagmi' +import { useConfig } from 'wagmi' import type { NetworkDef } from '@ui/utils' import { CurveContext, useWagmiWallet } from '@ui-kit/features/connect-wallet/lib/CurveContext' import { @@ -42,6 +43,7 @@ export const CurveProvider = ({ const { wallet, provider, isReconnecting } = useWagmiWallet() const isFocused = useIsDocumentFocused() const libKey = AppLibs[app] + const config = useConfig() useEffect(() => { /** @@ -69,7 +71,7 @@ export const CurveProvider = ({ const hydrateApp = async (lib: AppLib, prevLib?: AppLib) => { if (globalLibs.hydrated[app] != lib && hydrate[app]) { setConnectState(HYDRATING) - await hydrate[app](lib, prevLib, wallet) // if thrown, it will be caught in initLib + await hydrate[app](config, lib, prevLib, wallet) // if thrown, it will be caught in initLib } globalLibs.hydrated[app] = lib as (typeof globalLibs.hydrated)[App] setConnectState(SUCCESS) @@ -114,7 +116,7 @@ export const CurveProvider = ({ } void initLib() return () => abort.abort() - }, [app, hydrate, isReconnecting, libKey, network, wallet, walletChainId]) + }, [app, config, hydrate, isReconnecting, libKey, network, wallet, walletChainId]) // the following statements are skipping the render cycle, only update the libs when connectState changes too! const curveApi = globalLibs.getMatching('curveApi', wallet, network?.chainId) diff --git a/packages/curve-ui-kit/src/features/connect-wallet/lib/types.ts b/packages/curve-ui-kit/src/features/connect-wallet/lib/types.ts index 763135dfce..c7f5bb61a5 100644 --- a/packages/curve-ui-kit/src/features/connect-wallet/lib/types.ts +++ b/packages/curve-ui-kit/src/features/connect-wallet/lib/types.ts @@ -1,5 +1,6 @@ import type { Eip1193Provider } from 'ethers' import { Address } from 'viem' +import type { Config } from 'wagmi' import { type default as curveApi } from '@curvefi/api' import type { IChainId as CurveChainId, INetworkName as CurveNetworkId } from '@curvefi/api/lib/interfaces' import { type default as llamaApi } from '@curvefi/llamalend-api' @@ -55,6 +56,7 @@ export type AppChainId = LibChainId[(typeof AppLibs)[A]] export type AppNetworkId = LibNetworkId[(typeof AppLibs)[A]] export type Hydrator = ( + config: Config, lib: AppLib, prevLib: AppLib | undefined, wallet?: Wallet, From c5e33a5940022a4641de3b42fd35fa90c653ef70 Mon Sep 17 00:00:00 2001 From: Alunara Date: Thu, 11 Dec 2025 03:52:29 +0100 Subject: [PATCH 03/11] refactor: remove native currency symbol check --- .../src/dex/store/createUserBalancesSlice.ts | 1 - .../borrow/hooks/useCreateLoanForm.tsx | 2 +- .../borrow/hooks/useMaxTokenValues.tsx | 4 +-- .../manage-loan/LoanFormTokenInput.tsx | 1 - .../src/queries/token-balance.query.ts | 30 +++++-------------- 5 files changed, 11 insertions(+), 27 deletions(-) diff --git a/apps/main/src/dex/store/createUserBalancesSlice.ts b/apps/main/src/dex/store/createUserBalancesSlice.ts index 9a6278b687..e405ce0e89 100644 --- a/apps/main/src/dex/store/createUserBalancesSlice.ts +++ b/apps/main/src/dex/store/createUserBalancesSlice.ts @@ -67,7 +67,6 @@ const createUserBalancesSlice = ( chainId, userAddress: signerAddress, tokenAddress: token as Address, - tokenSymbol: '', }).then((balance) => [token, balance]), ), ) diff --git a/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx b/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx index 9a7792c955..e84280eac0 100644 --- a/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx +++ b/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx @@ -76,7 +76,7 @@ export function useCreateLoanForm({ params, isPending: form.formState.isSubmitting || isCreating, onSubmit: form.handleSubmit(onSubmit), // todo: handle form errors - maxTokenValues: useMaxTokenValues(collateralToken, params, form), + maxTokenValues: useMaxTokenValues(collateralToken?.address, params, form), borrowToken, collateralToken, isCreated, diff --git a/apps/main/src/llamalend/features/borrow/hooks/useMaxTokenValues.tsx b/apps/main/src/llamalend/features/borrow/hooks/useMaxTokenValues.tsx index 14e7841bf2..860bf67b2f 100644 --- a/apps/main/src/llamalend/features/borrow/hooks/useMaxTokenValues.tsx +++ b/apps/main/src/llamalend/features/borrow/hooks/useMaxTokenValues.tsx @@ -18,7 +18,7 @@ import type { BorrowForm, BorrowFormQueryParams } from '../types' * @param form - The react-hook-form instance managing the borrow form state. */ export function useMaxTokenValues( - collateralToken: { address: Address; symbol?: string } | undefined, + collateralToken: Address | undefined, params: BorrowFormQueryParams & { userAddress?: Address }, form: UseFormReturn, ) { @@ -26,7 +26,7 @@ export function useMaxTokenValues( data: userBalance, error: balanceError, isLoading: isBalanceLoading, - } = useTokenBalance({ ...params, tokenAddress: collateralToken?.address, tokenSymbol: collateralToken?.symbol }) + } = useTokenBalance({ ...params, tokenAddress: collateralToken }) const { data: maxBorrow, error: maxBorrowError, isLoading: isLoadingMaxBorrow } = useCreateLoanMaxReceive(params) const { data: maxTotalLeverage, diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx index b6611ac7a4..b1a7eb1ed1 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx @@ -53,7 +53,6 @@ export const LoanFormTokenInput = < chainId: network?.chainId, userAddress, tokenAddress: token?.address, - tokenSymbol: token?.symbol, }) const errors = form.formState.errors as PartialRecord, Error> diff --git a/packages/curve-ui-kit/src/queries/token-balance.query.ts b/packages/curve-ui-kit/src/queries/token-balance.query.ts index ca8d54aa32..d9b4a4ddf4 100644 --- a/packages/curve-ui-kit/src/queries/token-balance.query.ts +++ b/packages/curve-ui-kit/src/queries/token-balance.query.ts @@ -1,4 +1,4 @@ -import { erc20Abi, ethAddress, formatUnits, type Address } from 'viem' +import { erc20Abi, ethAddress, formatUnits, isAddressEqual, type Address } from 'viem' import { useConfig } from 'wagmi' import { useBalance, useReadContracts } from 'wagmi' import type { FieldsOf } from '@ui-kit/lib' @@ -10,7 +10,6 @@ import type { GetBalanceReturnType } from '@wagmi/core' import { getBalanceQueryOptions, readContractsQueryOptions } from '@wagmi/core/query' type TokenQuery = { tokenAddress: Address } -type TokenSymbolQuery = { tokenSymbol: string } /** Convert user collateral from GetBalanceReturnType to number */ const convertBalance = ({ value, decimals }: Partial) => @@ -30,20 +29,12 @@ const getERC20QueryContracts = ({ chainId, userAddress, tokenAddress }: ChainQue { chainId, address: tokenAddress, abi: erc20Abi, functionName: 'decimals' }, ] as const -/** Function that gets the native currency symbol for a given chain */ -const getNativeCurrencySymbol = (config: Config, chainId: number) => - config.chains.find((chain) => chain.id === chainId)?.nativeCurrency?.symbol - -/** Function that checks if a given token address and symbol are the chain's native currency symbol */ -const isNative = (config: Config, { chainId, tokenAddress, tokenSymbol }: ChainQuery & TokenQuery & TokenSymbolQuery) => - tokenAddress === ethAddress || tokenSymbol === getNativeCurrencySymbol(config, chainId) +/** In the Curve ecosystem all native chain gas tokens are the 0xeee...eee address */ +const isNative = ({ tokenAddress }: TokenQuery) => isAddressEqual(tokenAddress, ethAddress) /** Imperatively fetch token balance */ -export const fetchTokenBalance = async ( - config: Config, - query: ChainQuery & UserQuery & TokenQuery & TokenSymbolQuery, -) => - isNative(config, query) +export const fetchTokenBalance = async (config: Config, query: ChainQuery & UserQuery & TokenQuery) => + isNative(query) ? await queryClient .fetchQuery(getNativeBalanceQueryOptions(config, query)) .then((balance) => convertBalance({ value: balance.value, decimals: balance.decimals })) @@ -57,16 +48,11 @@ export const fetchTokenBalance = async ( .then((balance) => convertBalance({ value: balance[0], decimals: balance[1] })) /** Hook to fetch the token balance */ -export function useTokenBalance({ - chainId, - userAddress, - tokenAddress, - tokenSymbol, -}: FieldsOf) { +export function useTokenBalance({ chainId, userAddress, tokenAddress }: FieldsOf) { const config = useConfig() - const isEnabled = chainId != null && userAddress != null && tokenAddress != null && tokenSymbol != null - const isNativeToken = isEnabled && isNative(config, { chainId, tokenAddress, tokenSymbol }) + const isEnabled = chainId != null && userAddress != null && tokenAddress != null + const isNativeToken = isEnabled && isNative({ tokenAddress }) const nativeBalance = useBalance({ ...(isEnabled ? getNativeBalanceQueryOptions(config, { chainId, userAddress }) : {}), From 52841324ef8b07b8c87be5dfe6f2a9be1b8259bc Mon Sep 17 00:00:00 2001 From: Alunara Date: Thu, 11 Dec 2025 03:54:49 +0100 Subject: [PATCH 04/11] fix: always fetch a fresh result for fetchTokenBalance --- .../curve-ui-kit/src/queries/token-balance.query.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/curve-ui-kit/src/queries/token-balance.query.ts b/packages/curve-ui-kit/src/queries/token-balance.query.ts index d9b4a4ddf4..e62cc640dc 100644 --- a/packages/curve-ui-kit/src/queries/token-balance.query.ts +++ b/packages/curve-ui-kit/src/queries/token-balance.query.ts @@ -32,19 +32,20 @@ const getERC20QueryContracts = ({ chainId, userAddress, tokenAddress }: ChainQue /** In the Curve ecosystem all native chain gas tokens are the 0xeee...eee address */ const isNative = ({ tokenAddress }: TokenQuery) => isAddressEqual(tokenAddress, ethAddress) -/** Imperatively fetch token balance */ +/** Imperatively fetch token balance. Uses a staletime of 0 to always be guaranteed of a fresh result. */ export const fetchTokenBalance = async (config: Config, query: ChainQuery & UserQuery & TokenQuery) => isNative(query) ? await queryClient - .fetchQuery(getNativeBalanceQueryOptions(config, query)) + .fetchQuery({ ...getNativeBalanceQueryOptions(config, query), staleTime: 0 }) .then((balance) => convertBalance({ value: balance.value, decimals: balance.decimals })) : await queryClient - .fetchQuery( - readContractsQueryOptions(config, { + .fetchQuery({ + ...readContractsQueryOptions(config, { allowFailure: false, contracts: getERC20QueryContracts(query), }), - ) + staleTime: 0, + }) .then((balance) => convertBalance({ value: balance[0], decimals: balance[1] })) /** Hook to fetch the token balance */ From 8a0f819d9e5b4d88be42cccb01c76d372d8b69ef Mon Sep 17 00:00:00 2001 From: Alunara Date: Thu, 11 Dec 2025 04:21:59 +0100 Subject: [PATCH 05/11] refactor: isNativeToken only relies on tokenAddress --- packages/curve-ui-kit/src/queries/token-balance.query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/curve-ui-kit/src/queries/token-balance.query.ts b/packages/curve-ui-kit/src/queries/token-balance.query.ts index e62cc640dc..585c70006a 100644 --- a/packages/curve-ui-kit/src/queries/token-balance.query.ts +++ b/packages/curve-ui-kit/src/queries/token-balance.query.ts @@ -53,7 +53,7 @@ export function useTokenBalance({ chainId, userAddress, tokenAddress }: FieldsOf const config = useConfig() const isEnabled = chainId != null && userAddress != null && tokenAddress != null - const isNativeToken = isEnabled && isNative({ tokenAddress }) + const isNativeToken = tokenAddress != null && isNative({ tokenAddress }) const nativeBalance = useBalance({ ...(isEnabled ? getNativeBalanceQueryOptions(config, { chainId, userAddress }) : {}), From f24922208cc10ae3cbb758a4ce23a58e30dfc6c5 Mon Sep 17 00:00:00 2001 From: Alunara Date: Thu, 11 Dec 2025 04:44:53 +0100 Subject: [PATCH 06/11] fix: add missing allowFailure false in useTokenBalance --- .../curve-ui-kit/src/queries/token-balance.query.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/curve-ui-kit/src/queries/token-balance.query.ts b/packages/curve-ui-kit/src/queries/token-balance.query.ts index 585c70006a..04617205c8 100644 --- a/packages/curve-ui-kit/src/queries/token-balance.query.ts +++ b/packages/curve-ui-kit/src/queries/token-balance.query.ts @@ -61,6 +61,7 @@ export function useTokenBalance({ chainId, userAddress, tokenAddress }: FieldsOf }) const erc20Balance = useReadContracts({ + allowFailure: false, contracts: isEnabled ? getERC20QueryContracts({ chainId, userAddress, tokenAddress }) : undefined, query: { enabled: isEnabled && !isNativeToken }, }) @@ -73,12 +74,11 @@ export function useTokenBalance({ chainId, userAddress, tokenAddress }: FieldsOf } : { data: - erc20Balance.data && erc20Balance.data[0].status === 'success' && erc20Balance.data[1].status === 'success' - ? convertBalance({ - value: erc20Balance.data[0].result, - decimals: erc20Balance.data[1].result, - }) - : undefined, + erc20Balance.data && + convertBalance({ + value: erc20Balance.data[0], + decimals: erc20Balance.data[1], + }), error: erc20Balance.error, isLoading: erc20Balance.isLoading, } From 4d8b35f28c384dd72ce6557b37a9a9ab663837fc Mon Sep 17 00:00:00 2001 From: Alunara Date: Thu, 11 Dec 2025 23:29:39 +0100 Subject: [PATCH 07/11] chore: fix bigint serialization error when logging --- packages/curve-ui-kit/src/lib/logging.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/curve-ui-kit/src/lib/logging.ts b/packages/curve-ui-kit/src/lib/logging.ts index f58675eacf..2938d27e07 100644 --- a/packages/curve-ui-kit/src/lib/logging.ts +++ b/packages/curve-ui-kit/src/lib/logging.ts @@ -45,10 +45,14 @@ const getStatusStyle = (status: LogStatus) => { } } +// Wagmi uses bigints in query keys and args, so need to add support for serialization of bigints +const bigIntStringify = (obj: unknown) => + JSON.stringify(obj, (_, value) => (typeof value === 'bigint' ? value.toString() : value)) + function argToString(i: unknown, max = 200, trailing = 3) { let str try { - str = JSON.stringify(i) + str = bigIntStringify(i) } catch { return String(i) } @@ -78,7 +82,7 @@ export function log(key: LogKey, status?: LogStatus | unknown, ...args: unknown[ formattedString += '%c → ' styles.push('color: #666; font-size: 0.75em;') } - formattedString += `%c${typeof part === 'string' ? part : JSON.stringify(part)}` + formattedString += `%c${typeof part === 'string' ? part : bigIntStringify(part)}` styles.push('color: #4CAF50; font-weight: bold;') }) @@ -98,7 +102,7 @@ export function log(key: LogKey, status?: LogStatus | unknown, ...args: unknown[ } if (isCypress || typeof window === 'undefined') { // disable formatting when on cypress or server side. Electron prints logs to the output, but formatting breaks. - return logMethod(status)(JSON.stringify({ status, keyArray, args }).slice(0, 300)) + return logMethod(status)(bigIntStringify({ status, keyArray, args }).slice(0, 300)) } const hasDefinedStatus = status && Object.values(LogStatus).includes(status as LogStatus) From 7088f6b66a1e68c0456ede88e177d33b8f313613 Mon Sep 17 00:00:00 2001 From: Alunara Date: Thu, 11 Dec 2025 23:35:56 +0100 Subject: [PATCH 08/11] chore: add smol comment about lacking type inference of readContractsQueryOptions --- packages/curve-ui-kit/src/queries/token-balance.query.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/curve-ui-kit/src/queries/token-balance.query.ts b/packages/curve-ui-kit/src/queries/token-balance.query.ts index 04617205c8..21dbbe6ee9 100644 --- a/packages/curve-ui-kit/src/queries/token-balance.query.ts +++ b/packages/curve-ui-kit/src/queries/token-balance.query.ts @@ -60,6 +60,8 @@ export function useTokenBalance({ chainId, userAddress, tokenAddress }: FieldsOf query: { enabled: isEnabled && isNativeToken }, }) + // Spreading with ...readContractsQueryOptions() breaks Typescript's type inference, so we have to settle with the + // least common denominator that does *not* cause type inference issues, which is getERC20QueryContracts. const erc20Balance = useReadContracts({ allowFailure: false, contracts: isEnabled ? getERC20QueryContracts({ chainId, userAddress, tokenAddress }) : undefined, From 0d978bfc36aef402eaef3d1fefdce05d2c0e8b59 Mon Sep 17 00:00:00 2001 From: Alunara Date: Thu, 11 Dec 2025 23:41:46 +0100 Subject: [PATCH 09/11] refactor: add helper type TokenBalanceQuery --- packages/curve-ui-kit/src/queries/token-balance.query.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/curve-ui-kit/src/queries/token-balance.query.ts b/packages/curve-ui-kit/src/queries/token-balance.query.ts index 21dbbe6ee9..4346cdf540 100644 --- a/packages/curve-ui-kit/src/queries/token-balance.query.ts +++ b/packages/curve-ui-kit/src/queries/token-balance.query.ts @@ -10,6 +10,7 @@ import type { GetBalanceReturnType } from '@wagmi/core' import { getBalanceQueryOptions, readContractsQueryOptions } from '@wagmi/core/query' type TokenQuery = { tokenAddress: Address } +type TokenBalanceQuery = ChainQuery & UserQuery & TokenQuery /** Convert user collateral from GetBalanceReturnType to number */ const convertBalance = ({ value, decimals }: Partial) => @@ -23,7 +24,7 @@ const getNativeBalanceQueryOptions = (config: Config, { chainId, userAddress }: }) /** Create query contracts for ERC-20 token balance and decimals */ -const getERC20QueryContracts = ({ chainId, userAddress, tokenAddress }: ChainQuery & UserQuery & TokenQuery) => +const getERC20QueryContracts = ({ chainId, userAddress, tokenAddress }: TokenBalanceQuery) => [ { chainId, address: tokenAddress, abi: erc20Abi, functionName: 'balanceOf', args: [userAddress] }, { chainId, address: tokenAddress, abi: erc20Abi, functionName: 'decimals' }, @@ -33,7 +34,7 @@ const getERC20QueryContracts = ({ chainId, userAddress, tokenAddress }: ChainQue const isNative = ({ tokenAddress }: TokenQuery) => isAddressEqual(tokenAddress, ethAddress) /** Imperatively fetch token balance. Uses a staletime of 0 to always be guaranteed of a fresh result. */ -export const fetchTokenBalance = async (config: Config, query: ChainQuery & UserQuery & TokenQuery) => +export const fetchTokenBalance = async (config: Config, query: TokenBalanceQuery) => isNative(query) ? await queryClient .fetchQuery({ ...getNativeBalanceQueryOptions(config, query), staleTime: 0 }) @@ -49,7 +50,7 @@ export const fetchTokenBalance = async (config: Config, query: ChainQuery & User .then((balance) => convertBalance({ value: balance[0], decimals: balance[1] })) /** Hook to fetch the token balance */ -export function useTokenBalance({ chainId, userAddress, tokenAddress }: FieldsOf) { +export function useTokenBalance({ chainId, userAddress, tokenAddress }: FieldsOf) { const config = useConfig() const isEnabled = chainId != null && userAddress != null && tokenAddress != null From 47019c8402fff87677e71c234e298b369bb0c892 Mon Sep 17 00:00:00 2001 From: Alunara Date: Thu, 11 Dec 2025 23:59:41 +0100 Subject: [PATCH 10/11] feat: log the failing of fetching of tokens --- .../main/src/dex/store/createUserBalancesSlice.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/main/src/dex/store/createUserBalancesSlice.ts b/apps/main/src/dex/store/createUserBalancesSlice.ts index e405ce0e89..686cf283b8 100644 --- a/apps/main/src/dex/store/createUserBalancesSlice.ts +++ b/apps/main/src/dex/store/createUserBalancesSlice.ts @@ -61,17 +61,26 @@ const createUserBalancesSlice = ( sliceState.setStateByKey('loading', true) // This gets multicall batched by Wagmi and Viem internally - const balances = await Promise.all( + const balances = await Promise.allSettled( filteredBadTokens.map((token) => fetchTokenBalance(config, { chainId, userAddress: signerAddress, tokenAddress: token as Address, - }).then((balance) => [token, balance]), + }).then((balance) => [token, balance] as const), ), ) - const userBalancesMapper = Object.fromEntries(balances) + balances.forEach((result, index) => { + if (result.status === 'rejected') { + console.warn(`Failed to fetch balance for token ${filteredBadTokens[index]}:`, result.reason) + } + }) + + const userBalancesMapper = Object.fromEntries( + balances.filter((x) => x.status === 'fulfilled').map((x) => x.value), + ) + sliceState.setStateByKeys({ userBalancesMapper: { ...storedUserBalancesMapper, ...userBalancesMapper }, loading: false, From fdc614d281e0e6b45e123a72c6f510df61a9a997 Mon Sep 17 00:00:00 2001 From: Alunara Date: Fri, 12 Dec 2025 15:52:25 +0100 Subject: [PATCH 11/11] chore: rename stringify function --- packages/curve-ui-kit/src/lib/logging.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/curve-ui-kit/src/lib/logging.ts b/packages/curve-ui-kit/src/lib/logging.ts index 2938d27e07..acf712bafa 100644 --- a/packages/curve-ui-kit/src/lib/logging.ts +++ b/packages/curve-ui-kit/src/lib/logging.ts @@ -45,14 +45,14 @@ const getStatusStyle = (status: LogStatus) => { } } -// Wagmi uses bigints in query keys and args, so need to add support for serialization of bigints -const bigIntStringify = (obj: unknown) => +// Wagmi uses bigints in query keys and args, so need to add in serialization support +const stringify = (obj: unknown) => JSON.stringify(obj, (_, value) => (typeof value === 'bigint' ? value.toString() : value)) function argToString(i: unknown, max = 200, trailing = 3) { let str try { - str = bigIntStringify(i) + str = stringify(i) } catch { return String(i) } @@ -82,7 +82,7 @@ export function log(key: LogKey, status?: LogStatus | unknown, ...args: unknown[ formattedString += '%c → ' styles.push('color: #666; font-size: 0.75em;') } - formattedString += `%c${typeof part === 'string' ? part : bigIntStringify(part)}` + formattedString += `%c${typeof part === 'string' ? part : stringify(part)}` styles.push('color: #4CAF50; font-weight: bold;') }) @@ -102,7 +102,7 @@ export function log(key: LogKey, status?: LogStatus | unknown, ...args: unknown[ } if (isCypress || typeof window === 'undefined') { // disable formatting when on cypress or server side. Electron prints logs to the output, but formatting breaks. - return logMethod(status)(bigIntStringify({ status, keyArray, args }).slice(0, 300)) + return logMethod(status)(stringify({ status, keyArray, args }).slice(0, 300)) } const hasDefinedStatus = status && Object.values(LogStatus).includes(status as LogStatus)