From 65c7bdf94fca4372dd125e4b744c8e57bd0a7ff4 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Wed, 1 Apr 2026 23:17:08 +0800 Subject: [PATCH 1/3] fix: edit cap --- src/components/shared/token-icon.tsx | 14 +- .../vault-detail/settings/EditCaps.tsx | 186 +++++++++++++++--- .../components/rebalance/rebalance-modal.tsx | 103 +++++++++- 3 files changed, 266 insertions(+), 37 deletions(-) diff --git a/src/components/shared/token-icon.tsx b/src/components/shared/token-icon.tsx index e6425cde..abee4686 100644 --- a/src/components/shared/token-icon.tsx +++ b/src/components/shared/token-icon.tsx @@ -38,10 +38,11 @@ export function TokenIcon({ // If we have a token with an image, use that if (token?.img) { - const img = ( + const tokenImg = token.img; + const renderImage = () => ( {token.symbol} ); + const triggerImage = renderImage(); const title = customTooltipTitle ?? token.symbol; @@ -63,14 +65,16 @@ export function TokenIcon({ const secondaryDetail = customTooltipDetail && showTokenSource ? tokenSource : undefined; if (disableTooltip) { - return img; + return triggerImage; } return ( } > - {img} + {triggerImage} ); } diff --git a/src/features/autovault/components/vault-detail/settings/EditCaps.tsx b/src/features/autovault/components/vault-detail/settings/EditCaps.tsx index 3263d33a..5fcf8602 100644 --- a/src/features/autovault/components/vault-detail/settings/EditCaps.tsx +++ b/src/features/autovault/components/vault-detail/settings/EditCaps.tsx @@ -33,6 +33,59 @@ type MarketCapInfo = { existingCapId?: string; }; +function normalizeAddress(value: string | null | undefined): string | null { + if (typeof value !== 'string') return null; + const trimmed = value.trim(); + if (trimmed === '') return null; + return trimmed.toLowerCase(); +} + +function parseBigIntOrFallback(value: string | null | undefined, fallback: bigint, context: string): bigint { + if (value == null || value === '') return fallback; + + try { + return BigInt(value); + } catch (error) { + console.error('[EditCaps] invalid bigint value', { + context, + value, + error, + }); + return fallback; + } +} + +function hasCompleteEditableMarketMetadata(market: Market): boolean { + return ( + normalizeAddress(market.loanAsset?.address) !== null && + normalizeAddress(market.collateralAsset?.address) !== null && + normalizeAddress(market.oracleAddress) !== null && + normalizeAddress(market.irmAddress) !== null && + market.lltv !== undefined && + market.lltv !== '' + ); +} + +function areMarketCapsEqual(left: Map, right: Map): boolean { + if (left.size !== right.size) return false; + + for (const [key, leftValue] of left.entries()) { + const rightValue = right.get(key); + if (!rightValue) return false; + + if ( + leftValue.market.uniqueKey !== rightValue.market.uniqueKey || + leftValue.relativeCap !== rightValue.relativeCap || + leftValue.absoluteCap !== rightValue.absoluteCap || + leftValue.existingCapId !== rightValue.existingCapId + ) { + return false; + } + } + + return true; +} + export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdating, adapterAddress, onBack, onSave }: EditCapsProps) { const [marketCaps, setMarketCaps] = useState>(new Map()); const [removedMarketIds, setRemovedMarketIds] = useState>(new Set()); @@ -73,25 +126,32 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin // Filter available markets for adding const availableMarkets = useMemo(() => { - if (!markets || !vaultAsset) return []; - return markets.filter((m) => m.loanAsset.address.toLowerCase() === vaultAsset.toLowerCase() && m.morphoBlue.chain.id === chainId); - }, [markets, vaultAsset, chainId]); + const normalizedVaultAsset = normalizeAddress(vaultAsset); + if (!markets || !normalizedVaultAsset) return []; - // Initialize from existing caps (only on first load, not after user edits) - useEffect(() => { - // Don't reset state if user has made edits - prevents losing work on background refetch - if (hasUserEditsRef.current) return; - if (availableMarkets.length === 0) return; + return markets.filter((market) => { + return normalizeAddress(market.loanAsset?.address) === normalizedVaultAsset && market.morphoBlue.chain.id === chainId; + }); + }, [markets, vaultAsset, chainId]); + const initialMarketCaps = useMemo(() => { const marketCapsMap = new Map(); for (const cap of existingCaps?.marketCaps ?? []) { const parsed = parseCapIdParams(cap.idParams); const market = availableMarkets.find((m) => m.uniqueKey.toLowerCase() === parsed.marketId?.toLowerCase()); if (market) { - const relativeCapBigInt = BigInt(cap.relativeCap); + if (!hasCompleteEditableMarketMetadata(market)) { + console.error('[EditCaps] skipping market cap with incomplete market metadata', { + marketUniqueKey: market.uniqueKey, + capId: cap.capId, + }); + continue; + } + + const relativeCapBigInt = parseBigIntOrFallback(cap.relativeCap, 0n, `market:${cap.capId}:relativeCap`); const relativeCap = (Number(relativeCapBigInt) / 1e16).toString(); - const absoluteCapBigInt = BigInt(cap.absoluteCap); + const absoluteCapBigInt = parseBigIntOrFallback(cap.absoluteCap, maxUint128, `market:${cap.capId}:absoluteCap`); const absoluteCap = absoluteCapBigInt === 0n || absoluteCapBigInt >= maxUint128 ? '' @@ -105,14 +165,46 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin }); } } - setMarketCaps(marketCapsMap); + return marketCapsMap; }, [availableMarkets, existingCaps, vaultAssetDecimals]); + // Initialize from existing caps (only on first load, not after user edits) + useEffect(() => { + // Don't reset state if user has made edits - prevents losing work on background refetch + if (hasUserEditsRef.current) return; + + setMarketCaps((prev) => { + if (areMarketCapsEqual(prev, initialMarketCaps)) { + return prev; + } + + console.debug('[EditCaps] syncing initial market caps', { + previousSize: prev.size, + nextSize: initialMarketCaps.size, + }); + + return initialMarketCaps; + }); + }, [initialMarketCaps]); + const handleAddMarkets = useCallback((newMarkets: Market[]) => { + const validMarkets = newMarkets.filter(hasCompleteEditableMarketMetadata); + const skippedCount = newMarkets.length - validMarkets.length; + + if (skippedCount > 0) { + console.error('[EditCaps] skipped markets with incomplete metadata while adding caps', { + skippedCount, + skippedMarketKeys: newMarkets.filter((market) => !hasCompleteEditableMarketMetadata(market)).map((market) => market.uniqueKey), + }); + toast.error('Some selected markets are missing metadata and could not be added to caps.'); + } + + if (validMarkets.length === 0) return; + hasUserEditsRef.current = true; setMarketCaps((prev) => { const next = new Map(prev); - for (const market of newMarkets) { + for (const market of validMarkets) { next.set(market.uniqueKey.toLowerCase(), { market, relativeCap: '100', @@ -180,8 +272,14 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin return parsed.marketId?.toLowerCase() === info.market.uniqueKey.toLowerCase(); }); if (!existing) return false; - const existingRelative = (Number(BigInt(existing.relativeCap)) / 1e16).toString(); - const existingAbsoluteBigInt = BigInt(existing.absoluteCap); + const existingRelative = ( + Number(parseBigIntOrFallback(existing.relativeCap, 0n, `market:${existing.capId}:relativeCap:compare`)) / 1e16 + ).toString(); + const existingAbsoluteBigInt = parseBigIntOrFallback( + existing.absoluteCap, + maxUint128, + `market:${existing.capId}:absoluteCap:compare`, + ); const existingAbsolute = existingAbsoluteBigInt === 0n || existingAbsoluteBigInt >= maxUint128 ? '' @@ -209,8 +307,12 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin const targetRelativeCap = parseUnits('100', 16); const targetAbsoluteCap = maxUint128; - const currentRelativeCap = existingCaps?.adapterCap ? BigInt(existingCaps.adapterCap.relativeCap) : 0n; - const currentAbsoluteCap = existingCaps?.adapterCap ? BigInt(existingCaps.adapterCap.absoluteCap) : 0n; + const currentRelativeCap = existingCaps?.adapterCap + ? parseBigIntOrFallback(existingCaps.adapterCap.relativeCap, 0n, `adapter:${existingCaps.adapterCap.capId}:relativeCap`) + : 0n; + const currentAbsoluteCap = existingCaps?.adapterCap + ? parseBigIntOrFallback(existingCaps.adapterCap.absoluteCap, 0n, `adapter:${existingCaps.adapterCap.capId}:absoluteCap`) + : 0n; // Only update if not already at target values if (currentRelativeCap !== targetRelativeCap || currentAbsoluteCap !== targetAbsoluteCap) { @@ -230,7 +332,16 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin const activeCollaterals = new Set(); for (const [key, info] of marketCaps.entries()) { if (!removedMarketIds.has(key)) { - activeCollaterals.add(info.market.collateralAsset.address.toLowerCase()); + const collateralAddress = normalizeAddress(info.market.collateralAsset?.address); + if (!collateralAddress) { + console.error('[EditCaps] cannot save caps for market with missing collateral address', { + marketUniqueKey: info.market.uniqueKey, + }); + toast.error('Unable to save caps because one market is missing collateral metadata.'); + return; + } + + activeCollaterals.add(collateralAddress); } } @@ -241,8 +352,8 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin return parsed.collateralToken?.toLowerCase() === collateralAddr; }); - const oldRelativeCap = existing ? BigInt(existing.relativeCap) : 0n; - const oldAbsoluteCap = existing ? BigInt(existing.absoluteCap) : 0n; + const oldRelativeCap = existing ? parseBigIntOrFallback(existing.relativeCap, 0n, `collateral:${existing.capId}:relativeCap`) : 0n; + const oldAbsoluteCap = existing ? parseBigIntOrFallback(existing.absoluteCap, 0n, `collateral:${existing.capId}:absoluteCap`) : 0n; if (oldRelativeCap !== targetRelativeCap || oldAbsoluteCap !== targetAbsoluteCap) { const { params, id } = getCollateralCapId(collateralAddr as Address); @@ -262,8 +373,8 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin const parsed = parseCapIdParams(cap.idParams); const addr = parsed.collateralToken?.toLowerCase(); if (addr && !activeCollaterals.has(addr)) { - const oldRelativeCap = BigInt(cap.relativeCap); - const oldAbsoluteCap = BigInt(cap.absoluteCap); + const oldRelativeCap = parseBigIntOrFallback(cap.relativeCap, 0n, `collateral:${cap.capId}:relativeCap:remove`); + const oldAbsoluteCap = parseBigIntOrFallback(cap.absoluteCap, 0n, `collateral:${cap.capId}:absoluteCap:remove`); if (oldRelativeCap !== 0n || oldAbsoluteCap !== 0n) { const { params, id } = getCollateralCapId(addr as Address); @@ -309,11 +420,24 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin return parsed.marketId?.toLowerCase() === info.market.uniqueKey.toLowerCase(); }); - const oldRelativeCap = existingCap ? BigInt(existingCap.relativeCap) : 0n; - const oldAbsoluteCap = existingCap ? BigInt(existingCap.absoluteCap) : 0n; + const oldRelativeCap = existingCap + ? parseBigIntOrFallback(existingCap.relativeCap, 0n, `market:${existingCap.capId}:relativeCap:save`) + : 0n; + const oldAbsoluteCap = existingCap + ? parseBigIntOrFallback(existingCap.absoluteCap, 0n, `market:${existingCap.capId}:absoluteCap:save`) + : 0n; // Only include if changed if (oldRelativeCap !== newRelativeCapBigInt || oldAbsoluteCap !== newAbsoluteCapBigInt) { + if (!hasCompleteEditableMarketMetadata(info.market)) { + console.error('[EditCaps] cannot save cap for market with incomplete metadata', { + marketUniqueKey: info.market.uniqueKey, + capId: existingCap?.capId, + }); + toast.error('Unable to save caps because one market is missing required metadata.'); + return; + } + const marketParams = { loanToken: info.market.loanAsset.address as Address, collateralToken: info.market.collateralAsset.address as Address, @@ -409,8 +533,16 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin ); } - const relativeCapBigInt = BigInt(existingCaps.adapterCap!.relativeCap); - const absoluteCapBigInt = BigInt(existingCaps.adapterCap!.absoluteCap); + const relativeCapBigInt = parseBigIntOrFallback( + existingCaps.adapterCap!.relativeCap, + 0n, + `adapter:${existingCaps.adapterCap!.capId}:relativeCap:warning`, + ); + const absoluteCapBigInt = parseBigIntOrFallback( + existingCaps.adapterCap!.absoluteCap, + 0n, + `adapter:${existingCaps.adapterCap!.capId}:absoluteCap:warning`, + ); const isFullyAuthorized = relativeCapBigInt >= parseUnits('100', 16) && absoluteCapBigInt >= maxUint128; if (!isFullyAuthorized) { @@ -453,8 +585,8 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin { - const collateralAddr = info.market.collateralAsset.address.toLowerCase(); - const collateralInfo = collateralCapMap.get(collateralAddr); + const collateralAddr = normalizeAddress(info.market.collateralAsset?.address); + const collateralInfo = collateralAddr ? collateralCapMap.get(collateralAddr) : undefined; return { market: info.market, relativeCap: info.relativeCap, diff --git a/src/features/positions/components/rebalance/rebalance-modal.tsx b/src/features/positions/components/rebalance/rebalance-modal.tsx index f9de142f..66317619 100644 --- a/src/features/positions/components/rebalance/rebalance-modal.tsx +++ b/src/features/positions/components/rebalance/rebalance-modal.tsx @@ -91,6 +91,32 @@ function parseMaxAllocationInput(raw: string): number | null { return parsed; } +function getSmartPlannerMarketSignature(market: Market): string { + return [ + market.uniqueKey, + market.loanAsset.address.toLowerCase(), + market.collateralAsset.address.toLowerCase(), + market.oracleAddress?.toLowerCase() ?? '', + market.irmAddress?.toLowerCase() ?? '', + market.lltv ?? '', + market.state.rateAtTarget, + ].join(':'); +} + +function getSmartPlannerConstraintSignature(constraints: Record): string { + return Object.entries(constraints) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, value]) => `${key}:${value}`) + .join('|'); +} + +function getSmartPlannerGroupedPositionSignature(groupedPosition: GroupedPosition): string { + return groupedPosition.markets + .map((position) => `${position.market.uniqueKey}:${position.state.supplyAssets}:${position.state.supplyShares}`) + .sort() + .join('|'); +} + type PreviewRow = { id: string; label: ReactNode; @@ -137,6 +163,7 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch, const calcIdRef = useRef(0); const wasOpenRef = useRef(false); const syncIndicatorTimeoutRef = useRef | null>(null); + const smartPlannerEligibleMarketsRef = useRef([]); const toast = useStyledToast(); const { isAprDisplay, rebalanceDefaultMode } = useAppSettings(); @@ -175,6 +202,21 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch, ); }, [markets, groupedPosition.loanAssetAddress, groupedPosition.chainId]); + const smartPlannerEligibleMarketsSignature = useMemo( + () => eligibleMarkets.map(getSmartPlannerMarketSignature).sort().join('|'), + [eligibleMarkets], + ); + const smartPlannerSelectedMarketsSignature = useMemo(() => [...smartSelectedMarketKeys].sort().join('|'), [smartSelectedMarketKeys]); + const smartPlannerConstraintSignature = useMemo( + () => getSmartPlannerConstraintSignature(debouncedSmartMaxAllocationBps), + [debouncedSmartMaxAllocationBps], + ); + const smartPlannerGroupedPositionSignature = useMemo(() => getSmartPlannerGroupedPositionSignature(groupedPosition), [groupedPosition]); + + useEffect(() => { + smartPlannerEligibleMarketsRef.current = eligibleMarkets; + }, [eligibleMarkets, smartPlannerEligibleMarketsSignature]); + const currentSupplyByMarket = useMemo( () => new Map(groupedPosition.markets.map((position) => [position.market.uniqueKey, BigInt(position.state.supplyAssets)])), [groupedPosition.markets], @@ -241,28 +283,79 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch, } } + console.debug('[smart-rebalance] plan calculation start', { + calcId: id, + chainId: groupedPosition.chainId, + selectedMarketKeys: [...smartSelectedMarketKeys].sort(), + candidateMarketCount: smartPlannerEligibleMarketsRef.current.length, + constraintCount: Object.keys(constraints).length, + }); + void calculateSmartRebalancePlan({ groupedPosition, chainId: groupedPosition.chainId as SupportedNetworks, - candidateMarkets: eligibleMarkets, + candidateMarkets: smartPlannerEligibleMarketsRef.current, includedMarketKeys: smartSelectedMarketKeys, constraints, }) .then((plan) => { - if (id !== calcIdRef.current) return; + if (id !== calcIdRef.current) { + console.debug('[smart-rebalance] plan calculation ignored stale success', { + calcId: id, + latestCalcId: calcIdRef.current, + }); + return; + } + console.debug('[smart-rebalance] plan calculation success', { + calcId: id, + hasPlan: plan !== null, + totalMoved: plan?.totalMoved.toString() ?? '0', + deltaCount: plan?.deltas.length ?? 0, + }); setSmartPlan(plan); }) .catch((error: unknown) => { - if (id !== calcIdRef.current) return; + if (id !== calcIdRef.current) { + console.debug('[smart-rebalance] plan calculation ignored stale error', { + calcId: id, + latestCalcId: calcIdRef.current, + }); + return; + } setSmartPlan(null); const message = error instanceof Error ? error.message : 'Failed to calculate smart rebalance plan.'; + console.error('[smart-rebalance] plan calculation failed', { + calcId: id, + chainId: groupedPosition.chainId, + message, + error, + }); setSmartCalculationError(message); }) .finally(() => { - if (id !== calcIdRef.current) return; + if (id !== calcIdRef.current) { + console.debug('[smart-rebalance] plan calculation skipped stale finalize', { + calcId: id, + latestCalcId: calcIdRef.current, + }); + return; + } + console.debug('[smart-rebalance] plan calculation finalize', { + calcId: id, + }); setIsSmartCalculating(false); }); - }, [debouncedSmartMaxAllocationBps, eligibleMarkets, groupedPosition, isOpen, mode, smartSelectedMarketKeys]); + }, [ + debouncedSmartMaxAllocationBps, + groupedPosition, + isOpen, + mode, + smartPlannerConstraintSignature, + smartPlannerEligibleMarketsSignature, + smartPlannerGroupedPositionSignature, + smartPlannerSelectedMarketsSignature, + smartSelectedMarketKeys, + ]); const fmtAmount = useCallback( (value: bigint) => `${formatReadable(formatBalance(value, groupedPosition.loanAssetDecimals))} ${groupedPosition.loanAssetSymbol}`, From 63950ba023d25a2d7602f21b5b2ab93db50fcbcf Mon Sep 17 00:00:00 2001 From: antoncoding Date: Wed, 1 Apr 2026 23:18:53 +0800 Subject: [PATCH 2/3] chore: cleanup --- .../vault-detail/settings/EditCaps.tsx | 5 --- .../components/rebalance/rebalance-modal.tsx | 41 ++----------------- 2 files changed, 3 insertions(+), 43 deletions(-) diff --git a/src/features/autovault/components/vault-detail/settings/EditCaps.tsx b/src/features/autovault/components/vault-detail/settings/EditCaps.tsx index 5fcf8602..bb478961 100644 --- a/src/features/autovault/components/vault-detail/settings/EditCaps.tsx +++ b/src/features/autovault/components/vault-detail/settings/EditCaps.tsx @@ -178,11 +178,6 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin return prev; } - console.debug('[EditCaps] syncing initial market caps', { - previousSize: prev.size, - nextSize: initialMarketCaps.size, - }); - return initialMarketCaps; }); }, [initialMarketCaps]); diff --git a/src/features/positions/components/rebalance/rebalance-modal.tsx b/src/features/positions/components/rebalance/rebalance-modal.tsx index 66317619..14efccd5 100644 --- a/src/features/positions/components/rebalance/rebalance-modal.tsx +++ b/src/features/positions/components/rebalance/rebalance-modal.tsx @@ -283,14 +283,6 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch, } } - console.debug('[smart-rebalance] plan calculation start', { - calcId: id, - chainId: groupedPosition.chainId, - selectedMarketKeys: [...smartSelectedMarketKeys].sort(), - candidateMarketCount: smartPlannerEligibleMarketsRef.current.length, - constraintCount: Object.keys(constraints).length, - }); - void calculateSmartRebalancePlan({ groupedPosition, chainId: groupedPosition.chainId as SupportedNetworks, @@ -299,29 +291,11 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch, constraints, }) .then((plan) => { - if (id !== calcIdRef.current) { - console.debug('[smart-rebalance] plan calculation ignored stale success', { - calcId: id, - latestCalcId: calcIdRef.current, - }); - return; - } - console.debug('[smart-rebalance] plan calculation success', { - calcId: id, - hasPlan: plan !== null, - totalMoved: plan?.totalMoved.toString() ?? '0', - deltaCount: plan?.deltas.length ?? 0, - }); + if (id !== calcIdRef.current) return; setSmartPlan(plan); }) .catch((error: unknown) => { - if (id !== calcIdRef.current) { - console.debug('[smart-rebalance] plan calculation ignored stale error', { - calcId: id, - latestCalcId: calcIdRef.current, - }); - return; - } + if (id !== calcIdRef.current) return; setSmartPlan(null); const message = error instanceof Error ? error.message : 'Failed to calculate smart rebalance plan.'; console.error('[smart-rebalance] plan calculation failed', { @@ -333,16 +307,7 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch, setSmartCalculationError(message); }) .finally(() => { - if (id !== calcIdRef.current) { - console.debug('[smart-rebalance] plan calculation skipped stale finalize', { - calcId: id, - latestCalcId: calcIdRef.current, - }); - return; - } - console.debug('[smart-rebalance] plan calculation finalize', { - calcId: id, - }); + if (id !== calcIdRef.current) return; setIsSmartCalculating(false); }); }, [ From 3f699fafc34cd97d644b4a6bfd8f7f2307920852 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Wed, 1 Apr 2026 23:46:08 +0800 Subject: [PATCH 3/3] chore: remove unused changes --- src/components/shared/token-icon.tsx | 14 +- .../vault-detail/settings/EditCaps.tsx | 138 +++--------------- 2 files changed, 25 insertions(+), 127 deletions(-) diff --git a/src/components/shared/token-icon.tsx b/src/components/shared/token-icon.tsx index abee4686..e6425cde 100644 --- a/src/components/shared/token-icon.tsx +++ b/src/components/shared/token-icon.tsx @@ -38,11 +38,10 @@ export function TokenIcon({ // If we have a token with an image, use that if (token?.img) { - const tokenImg = token.img; - const renderImage = () => ( + const img = ( {token.symbol} ); - const triggerImage = renderImage(); const title = customTooltipTitle ?? token.symbol; @@ -65,16 +63,14 @@ export function TokenIcon({ const secondaryDetail = customTooltipDetail && showTokenSource ? tokenSource : undefined; if (disableTooltip) { - return triggerImage; + return img; } return ( } > - {triggerImage} + {img} ); } diff --git a/src/features/autovault/components/vault-detail/settings/EditCaps.tsx b/src/features/autovault/components/vault-detail/settings/EditCaps.tsx index bb478961..5716089d 100644 --- a/src/features/autovault/components/vault-detail/settings/EditCaps.tsx +++ b/src/features/autovault/components/vault-detail/settings/EditCaps.tsx @@ -33,39 +33,6 @@ type MarketCapInfo = { existingCapId?: string; }; -function normalizeAddress(value: string | null | undefined): string | null { - if (typeof value !== 'string') return null; - const trimmed = value.trim(); - if (trimmed === '') return null; - return trimmed.toLowerCase(); -} - -function parseBigIntOrFallback(value: string | null | undefined, fallback: bigint, context: string): bigint { - if (value == null || value === '') return fallback; - - try { - return BigInt(value); - } catch (error) { - console.error('[EditCaps] invalid bigint value', { - context, - value, - error, - }); - return fallback; - } -} - -function hasCompleteEditableMarketMetadata(market: Market): boolean { - return ( - normalizeAddress(market.loanAsset?.address) !== null && - normalizeAddress(market.collateralAsset?.address) !== null && - normalizeAddress(market.oracleAddress) !== null && - normalizeAddress(market.irmAddress) !== null && - market.lltv !== undefined && - market.lltv !== '' - ); -} - function areMarketCapsEqual(left: Map, right: Map): boolean { if (left.size !== right.size) return false; @@ -126,12 +93,8 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin // Filter available markets for adding const availableMarkets = useMemo(() => { - const normalizedVaultAsset = normalizeAddress(vaultAsset); - if (!markets || !normalizedVaultAsset) return []; - - return markets.filter((market) => { - return normalizeAddress(market.loanAsset?.address) === normalizedVaultAsset && market.morphoBlue.chain.id === chainId; - }); + if (!markets || !vaultAsset) return []; + return markets.filter((m) => m.loanAsset.address.toLowerCase() === vaultAsset.toLowerCase() && m.morphoBlue.chain.id === chainId); }, [markets, vaultAsset, chainId]); const initialMarketCaps = useMemo(() => { @@ -140,18 +103,10 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin const parsed = parseCapIdParams(cap.idParams); const market = availableMarkets.find((m) => m.uniqueKey.toLowerCase() === parsed.marketId?.toLowerCase()); if (market) { - if (!hasCompleteEditableMarketMetadata(market)) { - console.error('[EditCaps] skipping market cap with incomplete market metadata', { - marketUniqueKey: market.uniqueKey, - capId: cap.capId, - }); - continue; - } - - const relativeCapBigInt = parseBigIntOrFallback(cap.relativeCap, 0n, `market:${cap.capId}:relativeCap`); + const relativeCapBigInt = BigInt(cap.relativeCap); const relativeCap = (Number(relativeCapBigInt) / 1e16).toString(); - const absoluteCapBigInt = parseBigIntOrFallback(cap.absoluteCap, maxUint128, `market:${cap.capId}:absoluteCap`); + const absoluteCapBigInt = BigInt(cap.absoluteCap); const absoluteCap = absoluteCapBigInt === 0n || absoluteCapBigInt >= maxUint128 ? '' @@ -183,23 +138,10 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin }, [initialMarketCaps]); const handleAddMarkets = useCallback((newMarkets: Market[]) => { - const validMarkets = newMarkets.filter(hasCompleteEditableMarketMetadata); - const skippedCount = newMarkets.length - validMarkets.length; - - if (skippedCount > 0) { - console.error('[EditCaps] skipped markets with incomplete metadata while adding caps', { - skippedCount, - skippedMarketKeys: newMarkets.filter((market) => !hasCompleteEditableMarketMetadata(market)).map((market) => market.uniqueKey), - }); - toast.error('Some selected markets are missing metadata and could not be added to caps.'); - } - - if (validMarkets.length === 0) return; - hasUserEditsRef.current = true; setMarketCaps((prev) => { const next = new Map(prev); - for (const market of validMarkets) { + for (const market of newMarkets) { next.set(market.uniqueKey.toLowerCase(), { market, relativeCap: '100', @@ -267,14 +209,8 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin return parsed.marketId?.toLowerCase() === info.market.uniqueKey.toLowerCase(); }); if (!existing) return false; - const existingRelative = ( - Number(parseBigIntOrFallback(existing.relativeCap, 0n, `market:${existing.capId}:relativeCap:compare`)) / 1e16 - ).toString(); - const existingAbsoluteBigInt = parseBigIntOrFallback( - existing.absoluteCap, - maxUint128, - `market:${existing.capId}:absoluteCap:compare`, - ); + const existingRelative = (Number(BigInt(existing.relativeCap)) / 1e16).toString(); + const existingAbsoluteBigInt = BigInt(existing.absoluteCap); const existingAbsolute = existingAbsoluteBigInt === 0n || existingAbsoluteBigInt >= maxUint128 ? '' @@ -302,12 +238,8 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin const targetRelativeCap = parseUnits('100', 16); const targetAbsoluteCap = maxUint128; - const currentRelativeCap = existingCaps?.adapterCap - ? parseBigIntOrFallback(existingCaps.adapterCap.relativeCap, 0n, `adapter:${existingCaps.adapterCap.capId}:relativeCap`) - : 0n; - const currentAbsoluteCap = existingCaps?.adapterCap - ? parseBigIntOrFallback(existingCaps.adapterCap.absoluteCap, 0n, `adapter:${existingCaps.adapterCap.capId}:absoluteCap`) - : 0n; + const currentRelativeCap = existingCaps?.adapterCap ? BigInt(existingCaps.adapterCap.relativeCap) : 0n; + const currentAbsoluteCap = existingCaps?.adapterCap ? BigInt(existingCaps.adapterCap.absoluteCap) : 0n; // Only update if not already at target values if (currentRelativeCap !== targetRelativeCap || currentAbsoluteCap !== targetAbsoluteCap) { @@ -327,16 +259,7 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin const activeCollaterals = new Set(); for (const [key, info] of marketCaps.entries()) { if (!removedMarketIds.has(key)) { - const collateralAddress = normalizeAddress(info.market.collateralAsset?.address); - if (!collateralAddress) { - console.error('[EditCaps] cannot save caps for market with missing collateral address', { - marketUniqueKey: info.market.uniqueKey, - }); - toast.error('Unable to save caps because one market is missing collateral metadata.'); - return; - } - - activeCollaterals.add(collateralAddress); + activeCollaterals.add(info.market.collateralAsset.address.toLowerCase()); } } @@ -347,8 +270,8 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin return parsed.collateralToken?.toLowerCase() === collateralAddr; }); - const oldRelativeCap = existing ? parseBigIntOrFallback(existing.relativeCap, 0n, `collateral:${existing.capId}:relativeCap`) : 0n; - const oldAbsoluteCap = existing ? parseBigIntOrFallback(existing.absoluteCap, 0n, `collateral:${existing.capId}:absoluteCap`) : 0n; + const oldRelativeCap = existing ? BigInt(existing.relativeCap) : 0n; + const oldAbsoluteCap = existing ? BigInt(existing.absoluteCap) : 0n; if (oldRelativeCap !== targetRelativeCap || oldAbsoluteCap !== targetAbsoluteCap) { const { params, id } = getCollateralCapId(collateralAddr as Address); @@ -368,8 +291,8 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin const parsed = parseCapIdParams(cap.idParams); const addr = parsed.collateralToken?.toLowerCase(); if (addr && !activeCollaterals.has(addr)) { - const oldRelativeCap = parseBigIntOrFallback(cap.relativeCap, 0n, `collateral:${cap.capId}:relativeCap:remove`); - const oldAbsoluteCap = parseBigIntOrFallback(cap.absoluteCap, 0n, `collateral:${cap.capId}:absoluteCap:remove`); + const oldRelativeCap = BigInt(cap.relativeCap); + const oldAbsoluteCap = BigInt(cap.absoluteCap); if (oldRelativeCap !== 0n || oldAbsoluteCap !== 0n) { const { params, id } = getCollateralCapId(addr as Address); @@ -415,24 +338,11 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin return parsed.marketId?.toLowerCase() === info.market.uniqueKey.toLowerCase(); }); - const oldRelativeCap = existingCap - ? parseBigIntOrFallback(existingCap.relativeCap, 0n, `market:${existingCap.capId}:relativeCap:save`) - : 0n; - const oldAbsoluteCap = existingCap - ? parseBigIntOrFallback(existingCap.absoluteCap, 0n, `market:${existingCap.capId}:absoluteCap:save`) - : 0n; + const oldRelativeCap = existingCap ? BigInt(existingCap.relativeCap) : 0n; + const oldAbsoluteCap = existingCap ? BigInt(existingCap.absoluteCap) : 0n; // Only include if changed if (oldRelativeCap !== newRelativeCapBigInt || oldAbsoluteCap !== newAbsoluteCapBigInt) { - if (!hasCompleteEditableMarketMetadata(info.market)) { - console.error('[EditCaps] cannot save cap for market with incomplete metadata', { - marketUniqueKey: info.market.uniqueKey, - capId: existingCap?.capId, - }); - toast.error('Unable to save caps because one market is missing required metadata.'); - return; - } - const marketParams = { loanToken: info.market.loanAsset.address as Address, collateralToken: info.market.collateralAsset.address as Address, @@ -528,16 +438,8 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin ); } - const relativeCapBigInt = parseBigIntOrFallback( - existingCaps.adapterCap!.relativeCap, - 0n, - `adapter:${existingCaps.adapterCap!.capId}:relativeCap:warning`, - ); - const absoluteCapBigInt = parseBigIntOrFallback( - existingCaps.adapterCap!.absoluteCap, - 0n, - `adapter:${existingCaps.adapterCap!.capId}:absoluteCap:warning`, - ); + const relativeCapBigInt = BigInt(existingCaps.adapterCap!.relativeCap); + const absoluteCapBigInt = BigInt(existingCaps.adapterCap!.absoluteCap); const isFullyAuthorized = relativeCapBigInt >= parseUnits('100', 16) && absoluteCapBigInt >= maxUint128; if (!isFullyAuthorized) { @@ -580,8 +482,8 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin { - const collateralAddr = normalizeAddress(info.market.collateralAsset?.address); - const collateralInfo = collateralAddr ? collateralCapMap.get(collateralAddr) : undefined; + const collateralAddr = info.market.collateralAsset.address.toLowerCase(); + const collateralInfo = collateralCapMap.get(collateralAddr); return { market: info.market, relativeCap: info.relativeCap,