Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ type MarketCapInfo = {
existingCapId?: string;
};

function areMarketCapsEqual(left: Map<string, MarketCapInfo>, right: Map<string, MarketCapInfo>): 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<Map<string, MarketCapInfo>>(new Map());
const [removedMarketIds, setRemovedMarketIds] = useState<Set<string>>(new Set());
Expand Down Expand Up @@ -77,12 +97,7 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin
return markets.filter((m) => m.loanAsset.address.toLowerCase() === vaultAsset.toLowerCase() && m.morphoBlue.chain.id === chainId);
}, [markets, vaultAsset, chainId]);

// 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;

const initialMarketCaps = useMemo(() => {
const marketCapsMap = new Map<string, MarketCapInfo>();
for (const cap of existingCaps?.marketCaps ?? []) {
const parsed = parseCapIdParams(cap.idParams);
Expand All @@ -105,9 +120,23 @@ 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;
}

return initialMarketCaps;
});
}, [initialMarketCaps]);

const handleAddMarkets = useCallback((newMarkets: Market[]) => {
hasUserEditsRef.current = true;
setMarketCaps((prev) => {
Expand Down
62 changes: 60 additions & 2 deletions src/features/positions/components/rebalance/rebalance-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, number>): 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;
Expand Down Expand Up @@ -137,6 +163,7 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch,
const calcIdRef = useRef(0);
const wasOpenRef = useRef(false);
const syncIndicatorTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const smartPlannerEligibleMarketsRef = useRef<Market[]>([]);

const toast = useStyledToast();
const { isAprDisplay, rebalanceDefaultMode } = useAppSettings();
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -244,7 +286,7 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch,
void calculateSmartRebalancePlan({
groupedPosition,
chainId: groupedPosition.chainId as SupportedNetworks,
candidateMarkets: eligibleMarkets,
candidateMarkets: smartPlannerEligibleMarketsRef.current,
includedMarketKeys: smartSelectedMarketKeys,
constraints,
})
Expand All @@ -256,13 +298,29 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch,
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', {
calcId: id,
chainId: groupedPosition.chainId,
message,
error,
});
setSmartCalculationError(message);
})
.finally(() => {
if (id !== calcIdRef.current) return;
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}`,
Expand Down