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 b3b4eb8a2c..c8cf5c540d 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, @@ -141,7 +142,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 } } @@ -766,7 +767,7 @@ const createCreatePoolSlice = ( }), ) }, - deployPool: async (curve: CurveApi) => { + deployPool: async (config: Config, curve: CurveApi) => { const chainId = curve.chainId const { pools: { fetchNewPool, basePools }, @@ -867,7 +868,7 @@ const createCreatePoolSlice = ( }), ) - const poolData = await fetchNewPool(curve, poolId) + const poolData = await fetchNewPool(config, curve, poolId) if (poolData) { set( produce((state) => { @@ -944,7 +945,7 @@ const createCreatePoolSlice = ( }), ) - const poolData = await fetchNewPool(curve, poolId) + const poolData = await fetchNewPool(config, curve, poolId) if (poolData) { set( produce((state) => { @@ -1025,7 +1026,7 @@ const createCreatePoolSlice = ( }), ) - const poolData = await fetchNewPool(curve, poolId) + const poolData = await fetchNewPool(config, curve, poolId) if (poolData) { set( produce((state) => { @@ -1123,7 +1124,7 @@ const createCreatePoolSlice = ( }), ) - const poolData = await fetchNewPool(curve, poolId) + const poolData = await fetchNewPool(config, curve, poolId) if (poolData) { set( produce((state) => { @@ -1214,7 +1215,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,28 @@ 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.allSettled( + filteredBadTokens.map((token) => + fetchTokenBalance(config, { + chainId, + userAddress: signerAddress, + tokenAddress: token as Address, + }).then((balance) => [token, balance] as const), + ), + ) + + 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, @@ -65,9 +88,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/llamalend/features/borrow/hooks/useCreateLoanForm.tsx b/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx index e6d808cedf..e2f3e57be7 100644 --- a/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx +++ b/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx @@ -78,7 +78,7 @@ export function useCreateLoanForm({ params, isPending: form.formState.isSubmitting || isCreating, onSubmit: form.handleSubmit(onSubmit), - 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 9d8368a411..860bf67b2f 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' @@ -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, collateralToken) + } = 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 1fb4bb46bc..6819beab52 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 { HelperMessage, 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,11 @@ export const LoanFormTokenInput = < data: balance, isLoading: isBalanceLoading, error: balanceError, - } = useTokenBalance({ chainId: network?.chainId, userAddress }, token) + } = useTokenBalance({ + chainId: network?.chainId, + userAddress, + tokenAddress: token?.address, + }) const errors = form.formState.errors as PartialRecord, Error> const relatedMaxFieldError = max?.data && max?.fieldName && errors[max.fieldName] 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 d35cf103aa..4588c85592 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, 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/lib/logging.ts b/packages/curve-ui-kit/src/lib/logging.ts index f58675eacf..acf712bafa 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 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 = JSON.stringify(i) + str = stringify(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 : stringify(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)(stringify({ status, keyArray, args }).slice(0, 300)) } const hasDefinedStatus = status && Object.values(LogStatus).includes(status as LogStatus) 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..4346cdf540 --- /dev/null +++ b/packages/curve-ui-kit/src/queries/token-balance.query.ts @@ -0,0 +1,88 @@ +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' +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 TokenBalanceQuery = ChainQuery & UserQuery & TokenQuery + +/** 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 }: TokenBalanceQuery) => + [ + { chainId, address: tokenAddress, abi: erc20Abi, functionName: 'balanceOf', args: [userAddress] }, + { chainId, address: tokenAddress, abi: erc20Abi, functionName: 'decimals' }, + ] as const + +/** 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. Uses a staletime of 0 to always be guaranteed of a fresh result. */ +export const fetchTokenBalance = async (config: Config, query: TokenBalanceQuery) => + isNative(query) + ? await queryClient + .fetchQuery({ ...getNativeBalanceQueryOptions(config, query), staleTime: 0 }) + .then((balance) => convertBalance({ value: balance.value, decimals: balance.decimals })) + : await queryClient + .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 */ +export function useTokenBalance({ chainId, userAddress, tokenAddress }: FieldsOf) { + const config = useConfig() + + const isEnabled = chainId != null && userAddress != null && tokenAddress != null + const isNativeToken = tokenAddress != null && isNative({ tokenAddress }) + + const nativeBalance = useBalance({ + ...(isEnabled ? getNativeBalanceQueryOptions(config, { chainId, userAddress }) : {}), + 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, + query: { enabled: isEnabled && !isNativeToken }, + }) + + return isNativeToken + ? { + data: nativeBalance.data && convertBalance(nativeBalance.data), + error: nativeBalance.error, + isLoading: nativeBalance.isLoading, + } + : { + data: + erc20Balance.data && + convertBalance({ + value: erc20Balance.data[0], + decimals: erc20Balance.data[1], + }), + error: erc20Balance.error, + isLoading: erc20Balance.isLoading, + } +}