From 99f055a650c69130aba36abc2e1b3c4bbf7413f4 Mon Sep 17 00:00:00 2001 From: Alunara Date: Wed, 24 Sep 2025 02:08:26 +0200 Subject: [PATCH 1/3] refactor: replace collateral zustand stores with tanstack --- .../loan/components/PageLoanCreate/Page.tsx | 72 ++++++------ .../loan/components/PageLoanManage/Page.tsx | 59 +++++----- apps/main/src/loan/entities/mint-markets.ts | 42 +++++++ apps/main/src/loan/hooks/useLoanAppStats.tsx | 23 ++-- apps/main/src/loan/lib/apiCrvusd.ts | 18 --- apps/main/src/loan/store/createAppSlice.ts | 8 +- apps/main/src/loan/store/createCacheSlice.ts | 52 --------- .../src/loan/store/createCollateralsSlice.ts | 103 ------------------ apps/main/src/loan/store/createLoansSlice.ts | 16 +-- apps/main/src/loan/store/useStore.ts | 24 +--- apps/main/src/loan/types/loan.types.ts | 19 ---- apps/main/src/loan/utils/helpers.ts | 5 - apps/main/src/loan/utils/utilsLoan.ts | 2 +- 13 files changed, 135 insertions(+), 308 deletions(-) create mode 100644 apps/main/src/loan/entities/mint-markets.ts delete mode 100644 apps/main/src/loan/store/createCacheSlice.ts delete mode 100644 apps/main/src/loan/store/createCollateralsSlice.ts diff --git a/apps/main/src/loan/components/PageLoanCreate/Page.tsx b/apps/main/src/loan/components/PageLoanCreate/Page.tsx index 1837ab3129..68763d9a09 100644 --- a/apps/main/src/loan/components/PageLoanCreate/Page.tsx +++ b/apps/main/src/loan/components/PageLoanCreate/Page.tsx @@ -9,6 +9,7 @@ import ChartOhlcWrapper from '@/loan/components/ChartOhlcWrapper' import { MarketInformationComp } from '@/loan/components/MarketInformationComp' import LoanCreate from '@/loan/components/PageLoanCreate/index' import { hasLeverage } from '@/loan/components/PageLoanCreate/utils' +import { useMintMarket } from '@/loan/entities/mint-markets' import { useMarketDetails } from '@/loan/hooks/useMarketDetails' import useStore from '@/loan/store/useStore' import { type CollateralUrlParams, type LlamaApi, Llamma } from '@/loan/types/loan.types' @@ -48,12 +49,12 @@ const Page = () => { const { address } = useAccount() const [loaded, setLoaded] = useState(false) - const collateralDatasMapper = useStore((state) => state.collaterals.collateralDatasMapper[rChainId]) + const { data: market } = useMintMarket({ chainId: rChainId, marketId: rCollateralId }) + const marketId = market?.id ?? '' const pageLoaded = !isLoading(connectState) - const { llamma, llamma: { id: llammaId = '' } = {}, displayName } = collateralDatasMapper?.[rCollateralId] ?? {} const formValues = useStore((state) => state.loanCreate.formValues) - const { data: loanExists } = useLoanExists({ chainId: rChainId, marketId: llammaId, userAddress: address }) + const { data: loanExists } = useLoanExists({ chainId: rChainId, marketId, userAddress: address }) const isMdUp = useLayoutStore((state) => state.isMdUp) const isPageVisible = useLayoutStore((state) => state.isPageVisible) const fetchLoanDetails = useStore((state) => state.loans.fetchLoanDetails) @@ -65,10 +66,10 @@ const Page = () => { const maxSlippage = useUserProfileStore((state) => state.maxSlippage.crypto) - const isReady = !!collateralDatasMapper + const isReady = !!market const isLeverage = rFormType === 'leverage' - const marketDetails = useMarketDetails({ chainId: rChainId, llamma, llammaId }) + const marketDetails = useMarketDetails({ chainId: rChainId, llamma: market, llammaId: marketId }) const fetchInitial = useCallback( (curve: LlamaApi, isLeverage: boolean, llamma: Llamma) => { @@ -95,48 +96,45 @@ const Page = () => { useEffect(() => { if (pageLoaded && curve?.hydrated) { - if (llamma) { - resetUserDetailsState(llamma) - fetchInitial(curve, isLeverage, llamma) - void fetchLoanDetails(curve, llamma) + if (market) { + resetUserDetailsState(market) + fetchInitial(curve, isLeverage, market) + void fetchLoanDetails(curve, market) setLoaded(true) - } else if (collateralDatasMapper) { - console.warn( - `Collateral ${rCollateralId} not found for chain ${rChainId}. Redirecting to market list.`, - collateralDatasMapper, - ) + } else { + console.warn(`Collateral ${rCollateralId} not found for chain ${rChainId}. Redirecting to market list.`) push(getCollateralListPathname(params)) } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pageLoaded && curve?.hydrated && llamma]) + }, [pageLoaded && curve?.hydrated && market]) // redirect if loan exists useEffect(() => { - if (!loaded && llamma && loanExists) { - push(getLoanManagePathname(params, llamma.id, 'loan')) + if (!loaded && market && loanExists) { + push(getLoanManagePathname(params, market.id, 'loan')) } - }, [llamma, loaded, loanExists, params, push]) + }, [loaded, loanExists, market, params, push]) // redirect if form is leverage but no leverage option useEffect(() => { - if (llamma && rFormType === 'leverage' && !hasLeverage(llamma)) { - push(getLoanCreatePathname(params, llamma.id)) + if (market && rFormType === 'leverage' && !hasLeverage(market)) { + push(getLoanCreatePathname(params, market.id)) } - }, [loaded, rFormType, llamma, push, params]) + }, [loaded, rFormType, market, push, params]) // max slippage updated useEffect(() => { - if (loaded && !!curve) { - void setFormValues(curve, isLeverage, llamma, formValues, maxSlippage) + if (loaded && !!curve && market) { + void setFormValues(curve, isLeverage, market, formValues, maxSlippage) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [maxSlippage]) usePageVisibleInterval( () => { - if (isPageVisible && curve && llamma) { - void fetchLoanDetails(curve, llamma) + if (isPageVisible && curve && market) { + void fetchLoanDetails(curve, market) } }, REFRESH_INTERVAL['1m'], @@ -151,7 +149,7 @@ const Page = () => { const TitleComp = () => ( - {displayName || getTokenName(llamma).collateral} + {market?.id === 'sfrxeth2' ? 'sfrxETH v2' : getTokenName(market).collateral} ) @@ -172,7 +170,7 @@ const Page = () => { - + )} @@ -184,8 +182,8 @@ const Page = () => { isReady={isReady} isLeverage={isLeverage} loanExists={loanExists} - llamma={llamma} - llammaId={llammaId} + llamma={market ?? null} + llammaId={marketId} params={params} rChainId={rChainId} rCollateralId={rCollateralId} @@ -202,13 +200,15 @@ const Page = () => { )} - + {market && ( + + )} diff --git a/apps/main/src/loan/components/PageLoanManage/Page.tsx b/apps/main/src/loan/components/PageLoanManage/Page.tsx index 673fdac853..bdbec44843 100644 --- a/apps/main/src/loan/components/PageLoanManage/Page.tsx +++ b/apps/main/src/loan/components/PageLoanManage/Page.tsx @@ -10,6 +10,7 @@ import { MarketInformationComp } from '@/loan/components/MarketInformationComp' import LoanMange from '@/loan/components/PageLoanManage/index' import type { FormType } from '@/loan/components/PageLoanManage/types' import { hasDeleverage } from '@/loan/components/PageLoanManage/utils' +import { useMintMarket } from '@/loan/entities/mint-markets' import { useLoanPositionDetails } from '@/loan/hooks/useLoanPositionDetails' import { useMarketDetails } from '@/loan/hooks/useMarketDetails' import useTitleMapper from '@/loan/hooks/useTitleMapper' @@ -47,12 +48,12 @@ const Page = () => { const rChainId = useChainId(params) const { address } = useAccount() - const { llamma } = useStore((state) => state.collaterals.collateralDatasMapper[rChainId]?.[rCollateralId]) ?? {} - const llammaId = llamma?.id || '' + const { data: market } = useMintMarket({ chainId: rChainId, marketId: rCollateralId }) + const marketId = market?.id ?? '' const isMdUp = useLayoutStore((state) => state.isMdUp) const isPageVisible = useLayoutStore((state) => state.isPageVisible) - const { data: loanExists } = useLoanExists({ chainId: rChainId, marketId: llammaId, userAddress: address }) + const { data: loanExists } = useLoanExists({ chainId: rChainId, marketId, userAddress: address }) const fetchLoanDetails = useStore((state) => state.loans.fetchLoanDetails) const fetchUserLoanDetails = useStore((state) => state.loans.fetchUserLoanDetails) const resetUserDetailsState = useStore((state) => state.loans.resetUserDetailsState) @@ -62,28 +63,28 @@ const Page = () => { const [loaded, setLoaded] = useState(false) const isValidRouterParams = !!rChainId && !!rCollateralId && !!rFormType - const isReady = !!curve?.signerAddress && !!llamma + const isReady = !!curve?.signerAddress && !!market - const marketDetails = useMarketDetails({ chainId: rChainId, llamma, llammaId }) + const marketDetails = useMarketDetails({ chainId: rChainId, llamma: market, llammaId: marketId }) const positionDetails = useLoanPositionDetails({ chainId: rChainId, - llamma, - llammaId, + llamma: market, + llammaId: marketId, }) useEffect(() => { if (curve?.hydrated && pageLoaded) { - if (rChainId && rCollateralId && rFormType && curve.signerAddress && llamma) { + if (rChainId && rCollateralId && rFormType && curve.signerAddress && market) { void (async () => { - const fetchedLoanDetails = await fetchLoanDetails(curve, llamma) + const fetchedLoanDetails = await fetchLoanDetails(curve, market) if (!fetchedLoanDetails.loanExists) { - resetUserDetailsState(llamma) + resetUserDetailsState(market) push(getLoanCreatePathname(params, rCollateralId)) } setLoaded(true) })() } else { - const args = { rChainId, rCollateralId, rFormType, signer: curve.signerAddress, llamma } + const args = { rChainId, rCollateralId, rFormType, signer: curve.signerAddress, market } console.warn(`Cannot find market ${rCollateralId}, redirecting to list`, args) push(getCollateralListPathname(params)) } @@ -93,17 +94,17 @@ const Page = () => { // redirect if form is deleverage but no deleverage option useEffect(() => { - if (llamma && rFormType === 'deleverage' && !hasDeleverage(llamma)) { - push(getLoanCreatePathname(params, llamma.id)) + if (market && rFormType === 'deleverage' && !hasDeleverage(market)) { + push(getLoanCreatePathname(params, market.id)) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loaded, rFormType, llamma]) + }, [loaded, rFormType, market]) usePageVisibleInterval( () => { - if (isPageVisible && curve && !!curve.signerAddress && llamma && loanExists) { - void fetchLoanDetails(curve, llamma) - void fetchUserLoanDetails(curve, llamma) + if (isPageVisible && curve && !!curve.signerAddress && market && loanExists) { + void fetchLoanDetails(curve, market) + void fetchUserLoanDetails(curve, market) } }, REFRESH_INTERVAL['1m'], @@ -118,9 +119,9 @@ const Page = () => { const formProps = { curve, - isReady: !!curve?.signerAddress && !!llamma, - llamma, - llammaId, + isReady: !!curve?.signerAddress && !!market, + llamma: market ?? null, + llammaId: marketId, rChainId, } @@ -140,7 +141,7 @@ const Page = () => { - + )} @@ -169,13 +170,15 @@ const Page = () => { )} - + {market && ( + + )} diff --git a/apps/main/src/loan/entities/mint-markets.ts b/apps/main/src/loan/entities/mint-markets.ts new file mode 100644 index 0000000000..7ab5197064 --- /dev/null +++ b/apps/main/src/loan/entities/mint-markets.ts @@ -0,0 +1,42 @@ +import { useMemo } from 'react' +import { ChainId } from '@/loan/types/loan.types' +import { requireLib } from '@ui-kit/features/connect-wallet' +import { ChainParams, ChainQuery, queryFactory } from '@ui-kit/lib/model/query' +import { llamaApiValidationSuite } from '@ui-kit/lib/model/query/curve-api-validation' + +export const { useQuery: useMintMarkets, fetchQuery: fetchMintMarkets } = queryFactory({ + queryKey: ({ chainId }: ChainParams) => ['chain', { chainId }, 'mint-markets'] as const, + queryFn: async ({}: ChainQuery) => { + const api = requireLib('llamaApi') + return api.mintMarkets.getMarketList().map((name) => api.getMintMarket(name)) + }, + staleTime: '5m', + refetchInterval: '1m', + validationSuite: llamaApiValidationSuite, +}) + +/** + * Hook to get a specific mint market by its id or controller address. + * @param chainId The chain for which to get the mint market of. + * @param marketId Mint market id or controller address + * @returns The market instance, if found. + */ +export const useMintMarket = ({ chainId, marketId }: { chainId: ChainId; marketId: string }) => { + const { data: markets, isSuccess, error } = useMintMarkets({ chainId }) + + /** Create mappings from market name or controller id to market instance. */ + const mapping = useMemo( + () => + markets && + Object.fromEntries( + markets.flatMap((market) => [ + [market.id, market], + [market.controller, market], + ]), + ), + [markets], + ) + + const market = mapping?.[marketId] + return { data: market, isSuccess, error } +} diff --git a/apps/main/src/loan/hooks/useLoanAppStats.tsx b/apps/main/src/loan/hooks/useLoanAppStats.tsx index ff49b65f68..a7b3061925 100644 --- a/apps/main/src/loan/hooks/useLoanAppStats.tsx +++ b/apps/main/src/loan/hooks/useLoanAppStats.tsx @@ -7,42 +7,39 @@ import type { ChainId } from '@/loan/types/loan.types' import { formatNumber } from '@ui/utils' import { t } from '@ui-kit/lib/i18n' import { useTokenUsdRate, useTokenUsdRates } from '@ui-kit/lib/model/entities/token-usd-rate' +import { useMintMarkets } from '../entities/mint-markets' const hasKeys = (obj: Record | undefined | null): obj is Record => !!obj && Object.keys(obj).length > 0 function useTvl(chainId: ChainId | undefined) { - const collateralDatasMapper = useStore((state) => chainId && state.collaterals.collateralDatasMapper[chainId]) + const { data: markets = [] } = useMintMarkets({ chainId }) const loansDetailsMapper = useStore((state) => state.loans.detailsMapper) const collateralTokenAddresses = useMemo( - () => - Object.values(collateralDatasMapper || {}) - .map((data) => data?.llamma?.collateral) - .filter(Boolean) as string[], - [collateralDatasMapper], + () => markets.map((market) => market.collateral).filter(Boolean) as string[], + [markets], ) const { data: usdRates } = useTokenUsdRates({ chainId, tokenAddresses: collateralTokenAddresses }) return useMemo(() => { - if (!hasKeys(collateralDatasMapper) || !hasKeys(loansDetailsMapper) || Object.keys(usdRates).length === 0) { + if (!markets.length || !hasKeys(loansDetailsMapper) || Object.keys(usdRates).length === 0) { return '-' } let sum = 0 - for (const key in collateralDatasMapper) { - const collateralData = collateralDatasMapper[key] - const loanDetails = loansDetailsMapper[key] - if (!collateralData || !loanDetails) { + for (const market of markets) { + const loanDetails = loansDetailsMapper[market.id] + if (!loanDetails) { continue } const { totalCollateral, totalStablecoin } = loanDetails - const totalCollateralUsd = +(totalCollateral ?? '0') * usdRates[collateralData.llamma.collateral] + const totalCollateralUsd = +(totalCollateral ?? '0') * usdRates[market.collateral] sum += totalCollateralUsd + +(totalStablecoin ?? '0') } return sum > 0 ? formatNumber(sum, { currency: 'USD', notation: 'compact' }) : '-' - }, [collateralDatasMapper, loansDetailsMapper, usdRates]) + }, [loansDetailsMapper, markets, usdRates]) } export function useLoanAppStats(chainId: ChainId | undefined) { diff --git a/apps/main/src/loan/lib/apiCrvusd.ts b/apps/main/src/loan/lib/apiCrvusd.ts index 7e081b9103..b8346aeb19 100644 --- a/apps/main/src/loan/lib/apiCrvusd.ts +++ b/apps/main/src/loan/lib/apiCrvusd.ts @@ -48,24 +48,6 @@ const DEFAULT_PARAMETERS = { } const helpers = { - getLlammaObj: (api: LlamaApi, token: string) => { - log('getLlammaObj', token) - return api.getMintMarket(token) - }, - getLlammas: (curve: LlamaApi) => { - log('getCollaterals', curve.chainId) - const collaterals = curve.mintMarkets.getMarketList() - - // set mappers - const llammasMapper: { [llammaId: string]: Llamma } = {} - for (const idx in collaterals) { - const collateralName = collaterals[idx] - const llamma = curve.getMintMarket(collateralName) - llammasMapper[llamma.id] = llamma - } - - return llammasMapper - }, getUsdRate: async (api: LlamaApi, tokenAddress: string) => { log('getUsdRate', tokenAddress) const resp: { usdRate: string | number; error: string } = { usdRate: 0, error: '' } diff --git a/apps/main/src/loan/store/createAppSlice.ts b/apps/main/src/loan/store/createAppSlice.ts index dae7f71b6d..09327cc5ab 100644 --- a/apps/main/src/loan/store/createAppSlice.ts +++ b/apps/main/src/loan/store/createAppSlice.ts @@ -4,6 +4,7 @@ import type { GetState, SetState } from 'zustand' import { type State } from '@/loan/store/useStore' import { type ChainId, type LlamaApi, Wallet } from '@/loan/types/loan.types' import { log } from '@/loan/utils/helpers' +import { fetchMintMarkets } from '../entities/mint-markets' export type DefaultStateKeys = keyof typeof DEFAULT_STATE export type SliceKey = keyof State | '' @@ -44,7 +45,7 @@ const createAppSlice = (set: SetState, get: GetState): AppSlice => hydrate: async (curveApi, prevCurveApi, wallet) => { if (!curveApi) return - const { loans, campaigns, collaterals } = get() + const { loans, campaigns } = get() const isNetworkSwitched = !!prevCurveApi?.chainId && prevCurveApi.chainId !== curveApi.chainId const isUserSwitched = !!prevCurveApi?.signerAddress && prevCurveApi.signerAddress !== curveApi.signerAddress @@ -60,9 +61,8 @@ const createAppSlice = (set: SetState, get: GetState): AppSlice => loans.setStateByKey('userDetailsMapper', {}) } - // Check if curveApi is actually a Curve instance and not a LendingApi - const { collateralDatas } = await collaterals.fetchCollaterals(curveApi) - await loans.fetchLoansDetails(curveApi, collateralDatas) + const markets = await fetchMintMarkets({ chainId: curveApi.chainId as ChainId }) + await loans.fetchLoansDetails(curveApi, markets) if (!prevCurveApi || isNetworkSwitched) { campaigns.initCampaignRewards(curveApi.chainId as ChainId) diff --git a/apps/main/src/loan/store/createCacheSlice.ts b/apps/main/src/loan/store/createCacheSlice.ts deleted file mode 100644 index cf4af0d1b8..0000000000 --- a/apps/main/src/loan/store/createCacheSlice.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { GetState, SetState } from 'zustand' -import type { State } from '@/loan/store/useStore' -import { CollateralDataCacheMapper } from '@/loan/types/loan.types' -import { sleep } from '@/loan/utils/helpers' - -type StateKey = keyof typeof DEFAULT_STATE - -type SliceState = { - collateralDatasMapper: { [chainId: string]: CollateralDataCacheMapper } -} - -const sliceKey = 'storeCache' - -export type CacheSlice = { - [sliceKey]: SliceState & { - setStateByActiveKey(key: StateKey, activeKey: string, value: T): Promise - setStateByKey(key: StateKey, value: T): Promise - setStateByKeys(SliceState: Partial): Promise - resetState(): void - } -} - -const DEFAULT_STATE: SliceState = { - collateralDatasMapper: {}, -} - -const TIMEOUT_MS = 4000 - -const createCacheSlice = (set: SetState, get: GetState) => ({ - storeCache: { - ...DEFAULT_STATE, - - // slice helpers - setStateByActiveKey: async (key: StateKey, activeKey: string, value: T) => { - await sleep(TIMEOUT_MS) - get().setAppStateByActiveKey(sliceKey, key, activeKey, value) - }, - setStateByKey: async (key: StateKey, value: T) => { - await sleep(TIMEOUT_MS) - get().setAppStateByKey(sliceKey, key, value) - }, - setStateByKeys: async (sliceState: Partial) => { - await sleep(TIMEOUT_MS) - get().setAppStateByKeys(sliceKey, sliceState) - }, - resetState: () => { - get().resetAppState(sliceKey, DEFAULT_STATE) - }, - }, -}) - -export default createCacheSlice diff --git a/apps/main/src/loan/store/createCollateralsSlice.ts b/apps/main/src/loan/store/createCollateralsSlice.ts deleted file mode 100644 index 1fdf6eb2b8..0000000000 --- a/apps/main/src/loan/store/createCollateralsSlice.ts +++ /dev/null @@ -1,103 +0,0 @@ -import lodash from 'lodash' -import type { GetState, SetState } from 'zustand' -import networks from '@/loan/networks' -import type { State } from '@/loan/store/useStore' -import { - ChainId, - LlamaApi, - Llamma, - CollateralData, - CollateralDatasMapper, - CollateralDataCache, - CollateralDataCacheMapper, -} from '@/loan/types/loan.types' - -type StateKey = keyof typeof DEFAULT_STATE - -type SliceState = { - collateralDatasMapper: { [chainId: string]: CollateralDatasMapper } -} - -const sliceKey = 'collaterals' - -// prettier-ignore -export type CollateralsSlice = { - [sliceKey]: SliceState & { - fetchCollaterals(curve: LlamaApi): Promise<{ collateralDatasMapper: CollateralDatasMapper; collateralDatas: CollateralData[] }> - - setStateByActiveKey(key: StateKey, activeKey: string, value: T): void - setStateByKey(key: StateKey, value: T): void - setStateByKeys(SliceState: Partial): void - resetState(chainId: ChainId): void - } -} - -const DEFAULT_STATE: SliceState = { - collateralDatasMapper: {}, -} - -const createCollateralsSlice = (set: SetState, get: GetState) => ({ - [sliceKey]: { - ...DEFAULT_STATE, - - fetchCollaterals: async (curve: LlamaApi) => { - const chainId = curve.chainId as ChainId - const llammasMapper = networks[chainId].api.helpers.getLlammas(curve) - - // mapper - const collateralDatasMapper: CollateralDatasMapper = {} - const collateralDatasCacheMapper: CollateralDataCacheMapper = {} - - for (const key in llammasMapper) { - const llamma = llammasMapper[key] - const { collateralData, collateralDataCache } = getCollateralData(llamma) - collateralDatasMapper[key] = collateralData - collateralDatasCacheMapper[key] = collateralDataCache - } - - const collateralDatas = Object.entries(collateralDatasMapper).map(([_, v]) => v) - get()[sliceKey].setStateByActiveKey('collateralDatasMapper', chainId.toString(), collateralDatasMapper) - - // add to cache - void get().storeCache.setStateByActiveKey('collateralDatasMapper', chainId.toString(), collateralDatasCacheMapper) - - return { collateralDatas, collateralDatasMapper } - }, - - // slice helpers - setStateByActiveKey: (key: StateKey, activeKey: string, value: T) => { - get().setAppStateByActiveKey(sliceKey, key, activeKey, value) - }, - setStateByKey: (key: StateKey, value: T) => { - get().setAppStateByKey(sliceKey, key, value) - }, - setStateByKeys: (sliceState: Partial) => { - get().setAppStateByKeys(sliceKey, sliceState) - }, - resetState: () => { - get().resetAppState(sliceKey, lodash.cloneDeep(DEFAULT_STATE)) - }, - }, -}) - -function getCollateralData(llamma: Llamma) { - const collateralData: CollateralData = { - llamma, - displayName: llamma.id === 'sfrxeth2' ? 'sfrxETH v2' : '', - } - - const collateralDataCache = lodash.pick(collateralData, [ - 'llamma.id', - 'llamma.address', - 'llamma.controller', - 'llamma.collateral', - 'llamma.collateralSymbol', - 'llamma.coins', - 'llamma.coinAddresses', - 'displayName', - ]) as CollateralDataCache - - return { collateralData, collateralDataCache } -} - -export default createCollateralsSlice diff --git a/apps/main/src/loan/store/createLoansSlice.ts b/apps/main/src/loan/store/createLoansSlice.ts index f8180bf2ea..6e42f4a0bf 100644 --- a/apps/main/src/loan/store/createLoansSlice.ts +++ b/apps/main/src/loan/store/createLoansSlice.ts @@ -5,7 +5,6 @@ import networks from '@/loan/networks' import type { State } from '@/loan/store/useStore' import { type ChainId, - CollateralData, LlamaApi, Llamma, LoanDetails, @@ -13,6 +12,7 @@ import { UserLoanDetails, UserWalletBalances, } from '@/loan/types/loan.types' +import type { MintMarketTemplate } from '@curvefi/llamalend-api/lib/mintMarkets' import { PromisePool } from '@supercharge/promise-pool' import { log } from '@ui-kit/lib/logging' @@ -30,7 +30,7 @@ const sliceKey = 'loans' export type LoansSlice = { [sliceKey]: SliceState & { - fetchLoansDetails(curve: LlamaApi, collateralDatas: CollateralData[]): Promise + fetchLoansDetails(curve: LlamaApi, markets: MintMarketTemplate[]): Promise fetchLoanDetails(curve: LlamaApi, llamma: Llamma): Promise<{ loanDetails: LoanDetails; loanExists: boolean }> fetchUserLoanWalletBalances(curve: LlamaApi, llamma: Llamma): Promise fetchUserLoanDetails(curve: LlamaApi, llamma: Llamma): Promise @@ -56,16 +56,16 @@ const createLoansSlice = (set: SetState, get: GetState) => ({ [sliceKey]: { ...DEFAULT_STATE, - fetchLoansDetails: async (curve: LlamaApi, collateralDatas: CollateralData[]) => { + fetchLoansDetails: async (curve: LlamaApi, markets: MintMarketTemplate[]) => { const chainId = curve.chainId as ChainId - log('fetchLoansDetails', chainId, collateralDatas.map(({ llamma }) => llamma.id).join(',')) + log('fetchLoansDetails', chainId, markets.map((market) => market.id).join(',')) // TODO: handle errors - const { results } = await PromisePool.for(collateralDatas) - .handleError((error, { llamma }) => { - log(`Unable to get details ${llamma.id}, ${error}`) + const { results } = await PromisePool.for(markets) + .handleError((error, market) => { + log(`Unable to get details ${market.id}, ${error}`) }) - .process(async ({ llamma }) => Promise.all([networks[chainId].api.detailInfo.loanPartialInfo(llamma)])) + .process(async (market) => Promise.all([networks[chainId].api.detailInfo.loanPartialInfo(market)])) // mapper const loansDetailsMapper = lodash.cloneDeep(get()[sliceKey].detailsMapper ?? {}) diff --git a/apps/main/src/loan/store/useStore.ts b/apps/main/src/loan/store/useStore.ts index 495d53c7a2..6ec4a6ab8b 100644 --- a/apps/main/src/loan/store/useStore.ts +++ b/apps/main/src/loan/store/useStore.ts @@ -1,12 +1,9 @@ -import lodash from 'lodash' import type { GetState, SetState } from 'zustand' import { create } from 'zustand' -import { devtools, persist, type PersistOptions } from 'zustand/middleware' +import { devtools } from 'zustand/middleware' import createAppSlice, { AppSlice } from '@/loan/store/createAppSlice' -import createCacheSlice, { CacheSlice } from '@/loan/store/createCacheSlice' import createCampaignRewardsSlice, { CampaignRewardsSlice } from '@/loan/store/createCampaignRewardsSlice' import createChartBandsSlice, { ChartBandsSlice } from '@/loan/store/createChartBandsStore' -import createCollateralsSlice, { CollateralsSlice } from '@/loan/store/createCollateralsSlice' import createIntegrationsSlice, { IntegrationsSlice } from '@/loan/store/createIntegrationsSlice' import createLoanCollateralDecrease, { LoanCollateralDecreaseSlice, @@ -23,10 +20,8 @@ import createLoansSlice, { LoansSlice } from '@/loan/store/createLoansSlice' import createOhlcChartSlice, { OhlcChartSlice } from '@/loan/store/createOhlcChartSlice' import createScrvUsdSlice, { ScrvUsdSlice } from '@/loan/store/createScrvUsdSlice' -export type State = CacheSlice & - AppSlice & +export type State = AppSlice & ChartBandsSlice & - CollateralsSlice & LoansSlice & LoanCreateSlice & LoanCollateralDecreaseSlice & @@ -41,10 +36,8 @@ export type State = CacheSlice & CampaignRewardsSlice const store = (set: SetState, get: GetState): State => ({ - ...createCacheSlice(set, get), ...createAppSlice(set, get), ...createChartBandsSlice(set, get), - ...createCollateralsSlice(set, get), ...createLoansSlice(set, get), ...createLoanCreate(set, get), ...createLoanCollateralDecrease(set, get), @@ -59,17 +52,6 @@ const store = (set: SetState, get: GetState): State => ({ ...createCampaignRewardsSlice(set, get), }) -// cache all items in CacheSlice store - -const cache: PersistOptions> = { - name: 'crvusd-app-store-cache', - partialize: ({ storeCache }: State) => ({ storeCache }), - // @ts-ignore - merge: (persistedState, currentState) => lodash.merge(persistedState, currentState), - version: 2, // update version number to prevent UI from using cache -} - -const useStore = - process.env.NODE_ENV === 'development' ? create(devtools(persist(store, cache))) : create(persist(store, cache)) +const useStore = process.env.NODE_ENV === 'development' ? create(devtools(store)) : create(store) export default useStore diff --git a/apps/main/src/loan/types/loan.types.ts b/apps/main/src/loan/types/loan.types.ts index 53f4e95397..8847284d75 100644 --- a/apps/main/src/loan/types/loan.types.ts +++ b/apps/main/src/loan/types/loan.types.ts @@ -31,25 +31,6 @@ export interface NetworkConfig extends BaseConfig { export type Llamma = MintMarketTemplate -export interface CollateralData { - llamma: Llamma - displayName: string -} - -export type CollateralDatasMapper = { [collateralId: string]: CollateralData } -export type CollateralDataCache = { - llamma: { - id: string - address: string - controller: string - collateral: string - collateralSymbol: string - coins: string[] - coinAddresses: string[] - } - displayName: string -} -export type CollateralDataCacheMapper = { [collateralId: string]: CollateralDataCache } export type HealthColorKey = 'healthy' | 'close_to_liquidation' | 'soft_liquidation' | 'hard_liquidation' | '' export type HealthMode = { percent: string diff --git a/apps/main/src/loan/utils/helpers.ts b/apps/main/src/loan/utils/helpers.ts index 2c1ecd6514..a9ebedbef6 100644 --- a/apps/main/src/loan/utils/helpers.ts +++ b/apps/main/src/loan/utils/helpers.ts @@ -40,11 +40,6 @@ export function fulfilledValue(result: PromiseSettledResult) { export const httpFetcher = (uri: string) => fetch(uri).then((res) => res.json()) -export function sleep(ms?: number) { - const parsedMs = ms || Math.floor(Math.random() * (10000 - 1000 + 1) + 1000) - return new Promise((resolve) => setTimeout(resolve, parsedMs)) -} - export function curveProps(curve: LlamaApi | null) { if (curve) { const chainId = curve.chainId as ChainId diff --git a/apps/main/src/loan/utils/utilsLoan.ts b/apps/main/src/loan/utils/utilsLoan.ts index f97d10500a..d1b48ba96a 100644 --- a/apps/main/src/loan/utils/utilsLoan.ts +++ b/apps/main/src/loan/utils/utilsLoan.ts @@ -10,7 +10,7 @@ export function parseHealthPercent(healthPercent: string) { return formatNumber(healthPercent, { style: 'percent', maximumFractionDigits: 2 }) } -export function getTokenName(llamma: Llamma | null) { +export function getTokenName(llamma: Llamma | null | undefined) { const [stablecoin, collateral] = llamma?.coins ?? ['', ''] return { stablecoin, collateral } } From 62f3fc7d0e27c2b92689c153ae6ef10a87fa15b3 Mon Sep 17 00:00:00 2001 From: Alunara Date: Wed, 24 Sep 2025 19:59:07 +0200 Subject: [PATCH 2/3] fix: avoid using classes in tanstack --- .../loan/components/PageLoanCreate/Page.tsx | 2 +- .../loan/components/PageLoanManage/Page.tsx | 2 +- apps/main/src/loan/entities/mint-markets.ts | 75 +++++++++++-------- apps/main/src/loan/hooks/useLoanAppStats.tsx | 12 ++- apps/main/src/loan/store/createAppSlice.ts | 3 +- 5 files changed, 58 insertions(+), 36 deletions(-) diff --git a/apps/main/src/loan/components/PageLoanCreate/Page.tsx b/apps/main/src/loan/components/PageLoanCreate/Page.tsx index 68763d9a09..9efabcba9b 100644 --- a/apps/main/src/loan/components/PageLoanCreate/Page.tsx +++ b/apps/main/src/loan/components/PageLoanCreate/Page.tsx @@ -49,7 +49,7 @@ const Page = () => { const { address } = useAccount() const [loaded, setLoaded] = useState(false) - const { data: market } = useMintMarket({ chainId: rChainId, marketId: rCollateralId }) + const market = useMintMarket({ chainId: rChainId, marketId: rCollateralId }) const marketId = market?.id ?? '' const pageLoaded = !isLoading(connectState) diff --git a/apps/main/src/loan/components/PageLoanManage/Page.tsx b/apps/main/src/loan/components/PageLoanManage/Page.tsx index bdbec44843..471e8d5f80 100644 --- a/apps/main/src/loan/components/PageLoanManage/Page.tsx +++ b/apps/main/src/loan/components/PageLoanManage/Page.tsx @@ -48,7 +48,7 @@ const Page = () => { const rChainId = useChainId(params) const { address } = useAccount() - const { data: market } = useMintMarket({ chainId: rChainId, marketId: rCollateralId }) + const market = useMintMarket({ chainId: rChainId, marketId: rCollateralId }) const marketId = market?.id ?? '' const isMdUp = useLayoutStore((state) => state.isMdUp) diff --git a/apps/main/src/loan/entities/mint-markets.ts b/apps/main/src/loan/entities/mint-markets.ts index 7ab5197064..1e22a15e79 100644 --- a/apps/main/src/loan/entities/mint-markets.ts +++ b/apps/main/src/loan/entities/mint-markets.ts @@ -1,42 +1,57 @@ import { useMemo } from 'react' +import { isAddress } from 'viem' import { ChainId } from '@/loan/types/loan.types' -import { requireLib } from '@ui-kit/features/connect-wallet' -import { ChainParams, ChainQuery, queryFactory } from '@ui-kit/lib/model/query' -import { llamaApiValidationSuite } from '@ui-kit/lib/model/query/curve-api-validation' +import { fromEntries } from '@curvefi/prices-api/objects.util' +import { useConnection } from '@ui-kit/features/connect-wallet' +import type { Address } from '@ui-kit/utils' -export const { useQuery: useMintMarkets, fetchQuery: fetchMintMarkets } = queryFactory({ - queryKey: ({ chainId }: ChainParams) => ['chain', { chainId }, 'mint-markets'] as const, - queryFn: async ({}: ChainQuery) => { - const api = requireLib('llamaApi') - return api.mintMarkets.getMarketList().map((name) => api.getMintMarket(name)) - }, - staleTime: '5m', - refetchInterval: '1m', - validationSuite: llamaApiValidationSuite, -}) +/** + * Hook to get a mapping of mint market controller address to market name for all mint markets on a specific chain. + * Primarily useful fetching mint markets via URL. + */ +export const useMintMarketMapping = ({ chainId }: { chainId: ChainId | undefined }) => { + const { llamaApi: api } = useConnection() + const mapping = useMemo( + () => + api?.hydrated && + chainId && + fromEntries(api.mintMarkets.getMarketList().map((name) => [api.getMintMarket(name).controller as Address, name])), + // Need to specifically watch to api?.hydrated, as simply watching api as a whole won't trigger when hydrated is set to true by `useHydration` + // eslint-disable-next-line react-hooks/exhaustive-deps + [api?.hydrated, chainId], + ) + + return mapping || undefined +} /** * Hook to get a specific mint market by its id or controller address. - * @param chainId The chain for which to get the mint market of. + * @param chainId The chain for which to get the mint market of * @param marketId Mint market id or controller address * @returns The market instance, if found. */ -export const useMintMarket = ({ chainId, marketId }: { chainId: ChainId; marketId: string }) => { - const { data: markets, isSuccess, error } = useMintMarkets({ chainId }) +export const useMintMarket = ({ chainId, marketId }: { chainId: ChainId; marketId: string | Address }) => { + const marketMapping = useMintMarketMapping({ chainId }) + const { llamaApi: api } = useConnection() - /** Create mappings from market name or controller id to market instance. */ - const mapping = useMemo( - () => - markets && - Object.fromEntries( - markets.flatMap((market) => [ - [market.id, market], - [market.controller, market], - ]), - ), - [markets], - ) + return useMemo(() => { + if (!api) return undefined + + // If markets aren't found they throw an error, but we want to return undefined instead + const safeGetMarket = (id: string) => { + try { + return api.getMintMarket(id) + } catch { + return undefined + } + } + + // Try to get the market by name first + if (!isAddress(marketId)) return safeGetMarket(marketId) - const market = mapping?.[marketId] - return { data: market, isSuccess, error } + // Try to get by controller address mapping + return marketMapping && marketId in marketMapping && api.hydrated && api.chainId === chainId + ? safeGetMarket(marketMapping[marketId]) + : undefined + }, [api, chainId, marketId, marketMapping]) } diff --git a/apps/main/src/loan/hooks/useLoanAppStats.tsx b/apps/main/src/loan/hooks/useLoanAppStats.tsx index a7b3061925..eebed0d1f9 100644 --- a/apps/main/src/loan/hooks/useLoanAppStats.tsx +++ b/apps/main/src/loan/hooks/useLoanAppStats.tsx @@ -5,15 +5,23 @@ import { useAppStatsTotalCrvusdSupply } from '@/loan/entities/appstats-total-crv import useStore from '@/loan/store/useStore' import type { ChainId } from '@/loan/types/loan.types' import { formatNumber } from '@ui/utils' +import { useConnection } from '@ui-kit/features/connect-wallet' import { t } from '@ui-kit/lib/i18n' import { useTokenUsdRate, useTokenUsdRates } from '@ui-kit/lib/model/entities/token-usd-rate' -import { useMintMarkets } from '../entities/mint-markets' +import { useMintMarketMapping } from '../entities/mint-markets' const hasKeys = (obj: Record | undefined | null): obj is Record => !!obj && Object.keys(obj).length > 0 +/** Todo: we should replace this entire hook with prices API some day, there should be an endpoint available */ function useTvl(chainId: ChainId | undefined) { - const { data: markets = [] } = useMintMarkets({ chainId }) + const { llamaApi: api } = useConnection() + + const marketMapping = useMintMarketMapping({ chainId }) + const markets = useMemo( + () => (api && marketMapping ? Object.values(marketMapping).map(api.getMintMarket) : []), + [api, marketMapping], + ) const loansDetailsMapper = useStore((state) => state.loans.detailsMapper) const collateralTokenAddresses = useMemo( diff --git a/apps/main/src/loan/store/createAppSlice.ts b/apps/main/src/loan/store/createAppSlice.ts index 09327cc5ab..3e48c73fa4 100644 --- a/apps/main/src/loan/store/createAppSlice.ts +++ b/apps/main/src/loan/store/createAppSlice.ts @@ -4,7 +4,6 @@ import type { GetState, SetState } from 'zustand' import { type State } from '@/loan/store/useStore' import { type ChainId, type LlamaApi, Wallet } from '@/loan/types/loan.types' import { log } from '@/loan/utils/helpers' -import { fetchMintMarkets } from '../entities/mint-markets' export type DefaultStateKeys = keyof typeof DEFAULT_STATE export type SliceKey = keyof State | '' @@ -61,7 +60,7 @@ const createAppSlice = (set: SetState, get: GetState): AppSlice => loans.setStateByKey('userDetailsMapper', {}) } - const markets = await fetchMintMarkets({ chainId: curveApi.chainId as ChainId }) + const markets = curveApi.mintMarkets.getMarketList().map((name) => curveApi.getMintMarket(name)) await loans.fetchLoansDetails(curveApi, markets) if (!prevCurveApi || isNetworkSwitched) { From 2804725fcde8ac3a918d84198f0cfeaea8699f19 Mon Sep 17 00:00:00 2001 From: Alunara Date: Wed, 24 Sep 2025 20:07:11 +0200 Subject: [PATCH 3/3] chore: add comment on why there's no market title generalization --- apps/main/src/loan/components/PageLoanCreate/Page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/main/src/loan/components/PageLoanCreate/Page.tsx b/apps/main/src/loan/components/PageLoanCreate/Page.tsx index 9efabcba9b..6e24f645bd 100644 --- a/apps/main/src/loan/components/PageLoanCreate/Page.tsx +++ b/apps/main/src/loan/components/PageLoanCreate/Page.tsx @@ -149,6 +149,7 @@ const Page = () => { const TitleComp = () => ( + {/** TODO: generalize or re-use existing market counting technique, see `createCountMarket` in llama-markets.ts */} {market?.id === 'sfrxeth2' ? 'sfrxETH v2' : getTokenName(market).collateral} )