-
+
+ {showRebalanceModal && selectedGroupedPosition && (
+ setShowRebalanceModal(false)}
+ isOpen={showRebalanceModal}
+ />
+ )}
);
}
diff --git a/app/positions/components/RebalanceActionInput.tsx b/app/positions/components/RebalanceActionInput.tsx
new file mode 100644
index 00000000..d7cfe592
--- /dev/null
+++ b/app/positions/components/RebalanceActionInput.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import { Button } from '@nextui-org/react';
+import { ArrowRightIcon } from '@radix-ui/react-icons';
+import Image from 'next/image';
+import { Market } from '@/hooks/useMarkets';
+import { ERC20Token } from '@/utils/tokens';
+import { GroupedPosition } from '@/utils/types';
+import { MarketBadge } from './MarketBadge';
+
+type RebalanceActionInputProps = {
+ amount: string;
+ setAmount: (amount: string) => void;
+ selectedFromMarketUniqueKey: string;
+ selectedToMarketUniqueKey: string;
+ groupedPosition: GroupedPosition;
+ eligibleMarkets: Market[];
+ token: ERC20Token | undefined;
+ onAddAction: () => void;
+}
+
+export function RebalanceActionInput({
+ amount,
+ setAmount,
+ selectedFromMarketUniqueKey,
+ selectedToMarketUniqueKey,
+ groupedPosition,
+ eligibleMarkets,
+ token,
+ onAddAction,
+}: RebalanceActionInputProps) {
+ return (
+
+
Rebalance
+
setAmount(e.target.value)}
+ className="bg-hovered h-10 w-32 rounded p-2 focus:outline-none"
+ />
+
+ {groupedPosition.loanAsset}
+ {token?.img && (
+
+ )}
+
+
From
+
+ p.market.uniqueKey === selectedFromMarketUniqueKey)
+ ?.market
+ }
+ />
+
+
+
+ m.uniqueKey === selectedToMarketUniqueKey)}
+ />
+
+
+ Add Action
+
+
+ );
+}
diff --git a/app/positions/components/RebalanceCart.tsx b/app/positions/components/RebalanceCart.tsx
new file mode 100644
index 00000000..edef195d
--- /dev/null
+++ b/app/positions/components/RebalanceCart.tsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import {
+ Table,
+ TableHeader,
+ TableColumn,
+ TableBody,
+ TableRow,
+ TableCell,
+ Button,
+} from '@nextui-org/react';
+import { formatUnits } from 'viem';
+import { Market } from '@/hooks/useMarkets';
+import { GroupedPosition, RebalanceAction } from '@/utils/types';
+import { MarketBadge } from './MarketBadge';
+
+type RebalanceCartProps = {
+ rebalanceActions: RebalanceAction[];
+ groupedPosition: GroupedPosition;
+ eligibleMarkets: Market[];
+ removeRebalanceAction: (index: number) => void;
+}
+
+export function RebalanceCart({
+ rebalanceActions,
+ groupedPosition,
+ eligibleMarkets,
+ removeRebalanceAction,
+}: RebalanceCartProps) {
+ if (rebalanceActions.length === 0) {
+ return (
+
+ Your rebalance cart is empty. Add some actions!
+
+ );
+ }
+
+ return (
+ <>
+ Rebalance Cart
+
+
+ From Market
+ To Market
+ Amount
+ Actions
+
+
+ {rebalanceActions.map((action, index) => (
+
+
+ m.market.uniqueKey === action.fromMarket.uniqueKey,
+ )?.market
+ }
+ />
+
+
+ m.uniqueKey === action.toMarket.uniqueKey)}
+ />
+
+
+ {formatUnits(action.amount, groupedPosition.loanAssetDecimals)}{' '}
+ {groupedPosition.loanAsset}
+
+
+ removeRebalanceAction(index)}
+ className="rounded-sm bg-red-500 p-2 text-xs text-white duration-300 ease-in-out hover:bg-red-600 dark:bg-red-600 dark:hover:bg-red-700"
+ >
+ Remove
+
+
+
+ ))}
+
+
+ >
+ );
+}
diff --git a/app/positions/components/RebalanceModal.tsx b/app/positions/components/RebalanceModal.tsx
new file mode 100644
index 00000000..c5ca524c
--- /dev/null
+++ b/app/positions/components/RebalanceModal.tsx
@@ -0,0 +1,262 @@
+import React, { useState, useMemo, useCallback } from 'react';
+import {
+ Modal,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalFooter,
+ Button,
+} from '@nextui-org/react';
+import { toast } from 'react-toastify';
+import { parseUnits } from 'viem';
+import useMarkets, { Market } from '@/hooks/useMarkets';
+import { usePagination } from '@/hooks/usePagination';
+import { useRebalance } from '@/hooks/useRebalance';
+import { findToken } from '@/utils/tokens';
+import { GroupedPosition, RebalanceAction } from '@/utils/types';
+import { FromAndToMarkets } from './FromAndToMarkets';
+import { RebalanceActionInput } from './RebalanceActionInput';
+import { RebalanceCart } from './RebalanceCart';
+import { RebalanceProcessModal } from './RebalanceProcessModal';
+
+type RebalanceModalProps = {
+ groupedPosition: GroupedPosition;
+ isOpen: boolean;
+ onClose: () => void;
+};
+
+export const PER_PAGE = 5;
+
+export function RebalanceModal({ groupedPosition, isOpen, onClose }: RebalanceModalProps) {
+ const [fromMarketFilter, setFromMarketFilter] = useState('');
+ const [toMarketFilter, setToMarketFilter] = useState('');
+ const [selectedFromMarketUniqueKey, setSelectedFromMarketUniqueKey] = useState('');
+ const [selectedToMarketUniqueKey, setSelectedToMarketUniqueKey] = useState('');
+ const [amount, setAmount] = useState('0');
+ const [showProcessModal, setShowProcessModal] = useState(false);
+
+ const { data: allMarkets } = useMarkets();
+ const {
+ rebalanceActions,
+ addRebalanceAction,
+ removeRebalanceAction,
+ executeRebalance,
+ isConfirming,
+ currentStep,
+ } = useRebalance(groupedPosition);
+
+ const token = findToken(groupedPosition.loanAssetAddress, groupedPosition.chainId);
+ const fromPagination = usePagination();
+ const toPagination = usePagination();
+
+ const eligibleMarkets = useMemo(() => {
+ return allMarkets.filter(
+ (market) =>
+ market.loanAsset.address === groupedPosition.loanAssetAddress &&
+ market.morphoBlue.chain.id === groupedPosition.chainId,
+ );
+ }, [allMarkets, groupedPosition.loanAssetAddress, groupedPosition.chainId]);
+
+ const getPendingDelta = (marketUniqueKey: string) => {
+ return rebalanceActions.reduce((acc: number, action: RebalanceAction) => {
+ if (action.fromMarket.uniqueKey === marketUniqueKey) {
+ return acc - Number(action.amount);
+ }
+ if (action.toMarket.uniqueKey === marketUniqueKey) {
+ return acc + Number(action.amount);
+ }
+ return acc;
+ }, 0);
+ };
+
+ const validateInputs = () => {
+ if (!selectedFromMarketUniqueKey || !selectedToMarketUniqueKey || !amount) {
+ toast.error('Please fill in all fields');
+ return false;
+ }
+ const scaledAmount = parseUnits(amount, groupedPosition.loanAssetDecimals);
+ if (scaledAmount <= 0) {
+ toast.error('Amount must be greater than zero');
+ return false;
+ }
+ return true;
+ };
+
+ const getMarkets = () => {
+ const fromMarket = eligibleMarkets.find((m) => m.uniqueKey === selectedFromMarketUniqueKey);
+
+ const toMarket = eligibleMarkets.find((m) => m.uniqueKey === selectedToMarketUniqueKey);
+
+ if (!fromMarket || !toMarket) {
+ toast.error('Invalid market selection');
+ return null;
+ }
+
+ return { fromMarket, toMarket };
+ };
+
+ const checkBalance = () => {
+ const oldBalance = groupedPosition.markets.find(
+ (m) => m.market.uniqueKey === selectedFromMarketUniqueKey,
+ )?.supplyAssets;
+
+ const pendingDelta = getPendingDelta(selectedFromMarketUniqueKey);
+ const pendingBalance = BigInt(oldBalance ?? 0) + BigInt(pendingDelta);
+
+ const scaledAmount = parseUnits(amount, groupedPosition.loanAssetDecimals);
+ if (scaledAmount > pendingBalance) {
+ toast.error('Insufficient balance for this action');
+ return false;
+ }
+ return true;
+ };
+
+ const createAction = (fromMarket: Market, toMarket: Market): RebalanceAction => {
+ return {
+ fromMarket: {
+ loanToken: fromMarket.loanAsset.address,
+ collateralToken: fromMarket.collateralAsset.address,
+ oracle: fromMarket.oracleAddress,
+ irm: fromMarket.irmAddress,
+ lltv: fromMarket.lltv,
+ uniqueKey: fromMarket.uniqueKey,
+ },
+ toMarket: {
+ loanToken: toMarket.loanAsset.address,
+ collateralToken: toMarket.collateralAsset.address,
+ oracle: toMarket.oracleAddress,
+ irm: toMarket.irmAddress,
+ lltv: toMarket.lltv,
+ uniqueKey: toMarket.uniqueKey,
+ },
+ amount: parseUnits(amount, groupedPosition.loanAssetDecimals),
+ };
+ };
+
+ const resetSelections = () => {
+ setSelectedFromMarketUniqueKey('');
+ setSelectedToMarketUniqueKey('');
+ setAmount('0');
+ };
+
+ const handleAddAction = () => {
+ if (!validateInputs()) return;
+ const markets = getMarkets();
+ if (!markets) return;
+ const { fromMarket, toMarket } = markets;
+ if (!checkBalance()) return;
+ addRebalanceAction(createAction(fromMarket, toMarket));
+ resetSelections();
+ };
+
+ const handleExecuteRebalance = useCallback(async () => {
+ setShowProcessModal(true);
+ try {
+ await executeRebalance();
+ } catch (error) {
+ console.error('Error during rebalance:', error);
+ } finally {
+ setShowProcessModal(false);
+ }
+ }, [executeRebalance]);
+
+ return (
+ <>
+
+
+
+ Rebalance {groupedPosition.loanAsset ?? 'Unknown'} Position
+
+
+
+
+ Optimize your {groupedPosition.loanAsset} lending strategy by redistributing funds
+ across markets, add "Rebalance" actions to fine-tune your portfolio.
+
+
+
+
+
+ ({
+ ...market,
+ pendingDelta: getPendingDelta(market.market.uniqueKey),
+ }))}
+ toMarkets={eligibleMarkets}
+ fromFilter={fromMarketFilter}
+ toFilter={toMarketFilter}
+ onFromFilterChange={setFromMarketFilter}
+ onToFilterChange={setToMarketFilter}
+ onFromMarketSelect={setSelectedFromMarketUniqueKey}
+ onToMarketSelect={setSelectedToMarketUniqueKey}
+ fromPagination={{
+ currentPage: fromPagination.currentPage,
+ totalPages: Math.ceil(groupedPosition.markets.length / PER_PAGE),
+ onPageChange: fromPagination.setCurrentPage,
+ }}
+ toPagination={{
+ currentPage: toPagination.currentPage,
+ totalPages: Math.ceil(eligibleMarkets.length / PER_PAGE),
+ onPageChange: toPagination.setCurrentPage,
+ }}
+ selectedFromMarketUniqueKey={selectedFromMarketUniqueKey}
+ selectedToMarketUniqueKey={selectedToMarketUniqueKey}
+ />
+
+
+
+
+
+ Cancel
+
+ void handleExecuteRebalance()}
+ isDisabled={isConfirming || rebalanceActions.length === 0}
+ isLoading={isConfirming}
+ className="rounded-sm bg-orange-500 p-4 px-10 font-zen text-white opacity-80 transition-all duration-200 ease-in-out hover:scale-105 hover:opacity-100 disabled:opacity-50 dark:bg-orange-600"
+ >
+ Execute Rebalance
+
+
+
+
+ {showProcessModal && (
+ setShowProcessModal(false)}
+ tokenSymbol={groupedPosition.loanAsset}
+ actionsCount={rebalanceActions.length}
+ />
+ )}
+ >
+ );
+}
diff --git a/app/positions/components/RebalanceProcessModal.tsx b/app/positions/components/RebalanceProcessModal.tsx
new file mode 100644
index 00000000..db750cad
--- /dev/null
+++ b/app/positions/components/RebalanceProcessModal.tsx
@@ -0,0 +1,107 @@
+import React, { useMemo } from 'react';
+import { Cross1Icon } from '@radix-ui/react-icons';
+import { FaCheckCircle, FaCircle } from 'react-icons/fa';
+
+type RebalanceProcessModalProps = {
+ currentStep: 'idle' | 'approve' | 'authorize' | 'sign' | 'execute';
+ onClose: () => void;
+ tokenSymbol: string;
+ actionsCount: number;
+};
+
+export function RebalanceProcessModal({
+ currentStep,
+ onClose,
+ tokenSymbol,
+ actionsCount,
+}: RebalanceProcessModalProps): JSX.Element {
+ const steps = useMemo(
+ () => [
+ {
+ key: 'idle',
+ label: 'Idle',
+ detail: 'Waiting to start the rebalance process.',
+ },
+ {
+ key: 'approve',
+ label: 'Authorize Permit2',
+ detail: `This one-time approval ensures you don't need to send approval transactions in the future.`,
+ },
+ {
+ key: 'authorize',
+ label: 'Authorize Morpho Bundler',
+ detail: 'Authorize the Morpho official bundler to execute batched actions.',
+ },
+ {
+ key: 'sign',
+ label: 'Sign Permit',
+ detail: 'Sign a Permit2 signature to authorize the one time use of asset.',
+ },
+ {
+ key: 'execute',
+ label: 'Confirm Rebalance',
+ detail: `Confirm transaction in wallet to execute ${actionsCount} rebalance action${
+ actionsCount > 1 ? 's' : ''
+ }.`,
+ },
+ ],
+ [actionsCount],
+ );
+
+ const getStepStatus = (stepKey: string) => {
+ if (
+ steps.findIndex((step) => step.key === stepKey) <
+ steps.findIndex((step) => step.key === currentStep)
+ ) {
+ return 'done';
+ }
+ if (stepKey === currentStep) {
+ return 'current';
+ }
+ return 'undone';
+ };
+
+ return (
+
+
+
+ {' '}
+
+
+
+ Rebalancing {tokenSymbol} Positions
+
+
+
+ {steps
+ .filter((step) => step.key !== 'idle')
+ .map((step, index) => (
+
+
+ {getStepStatus(step.key) === 'done' &&
}
+ {getStepStatus(step.key) === 'current' &&
}
+ {getStepStatus(step.key) === 'undone' &&
}
+
+
+
{step.label}
+ {currentStep === step.key && step.detail && (
+
+ {step.detail}
+
+ )}
+
+ {index < steps.length - 2 &&
}
+
+ ))}
+
+
+
+ );
+}
diff --git a/app/positions/components/SuppliedMarketsDetail.tsx b/app/positions/components/SuppliedMarketsDetail.tsx
index d3b8938c..8d583df3 100644
--- a/app/positions/components/SuppliedMarketsDetail.tsx
+++ b/app/positions/components/SuppliedMarketsDetail.tsx
@@ -4,8 +4,7 @@ import Image from 'next/image';
import { formatReadable, formatBalance } from '@/utils/balance';
import { getMarketURL } from '@/utils/external';
import { findToken } from '@/utils/tokens';
-import { MarketPosition } from '@/utils/types';
-import { GroupedPosition } from './PositionsSummaryTable';
+import { MarketPosition, GroupedPosition } from '@/utils/types';
type SuppliedMarketsDetailProps = {
groupedPosition: GroupedPosition;
@@ -28,7 +27,7 @@ export function SuppliedMarketsDetail({
const totalSupply = groupedPosition.totalSupply;
return (
-
+
@@ -68,7 +67,6 @@ export function SuppliedMarketsDetail({
{position.market.collateralAsset ? (
- {position.market.collateralAsset.symbol}
{findToken(
position.market.collateralAsset.address,
position.market.morphoBlue.chain.id,
@@ -85,6 +83,7 @@ export function SuppliedMarketsDetail({
height={18}
/>
)}
+ {position.market.collateralAsset.symbol}
) : (
'N/A'
diff --git a/src/components/layout/header/AccountConnect.tsx b/src/components/layout/header/AccountConnect.tsx
index d7a0a55a..7274d934 100644
--- a/src/components/layout/header/AccountConnect.tsx
+++ b/src/components/layout/header/AccountConnect.tsx
@@ -38,7 +38,7 @@ function AccountConnect() {
Connect
diff --git a/src/hooks/useRebalance.ts b/src/hooks/useRebalance.ts
new file mode 100644
index 00000000..713e0223
--- /dev/null
+++ b/src/hooks/useRebalance.ts
@@ -0,0 +1,305 @@
+import { useState, useCallback } from 'react';
+import { toast } from 'react-toastify';
+import { Address, encodeFunctionData, maxUint256, parseSignature } from 'viem';
+import { useAccount, useReadContract, useSignTypedData } from 'wagmi';
+import morphoBundlerAbi from '@/abis/bundlerV2';
+import morphoAbi from '@/abis/morpho';
+import { useTransactionWithToast } from '@/hooks/useTransactionWithToast';
+import { getBundlerV2, MORPHO } from '@/utils/morpho';
+import { GroupedPosition, RebalanceAction } from '@/utils/types';
+import { usePermit2 } from './usePermit2';
+
+export const useRebalance = (groupedPosition: GroupedPosition) => {
+ const [rebalanceActions, setRebalanceActions] = useState([]);
+ const [isConfirming, setIsConfirming] = useState(false);
+ const [currentStep, setCurrentStep] = useState<
+ 'idle' | 'approve' | 'authorize' | 'sign' | 'execute'
+ >('idle');
+
+ const { address: account } = useAccount();
+ const { signTypedDataAsync } = useSignTypedData();
+ const bundlerAddress = getBundlerV2(groupedPosition.chainId);
+
+ const { data: isAuthorized } = useReadContract({
+ address: MORPHO,
+ abi: morphoAbi,
+ functionName: 'isAuthorized',
+ args: [account as Address, bundlerAddress as Address],
+ chainId: groupedPosition.chainId,
+ });
+
+ const { data: nonce } = useReadContract({
+ address: MORPHO,
+ abi: morphoAbi,
+ functionName: 'nonce',
+ args: [account as Address],
+ chainId: groupedPosition.chainId,
+ });
+
+ const totalAmount = rebalanceActions.reduce(
+ (acc, action) => acc + BigInt(action.amount),
+ BigInt(0),
+ );
+
+ const { authorizePermit2, permit2Authorized, signForBundlers } = usePermit2({
+ user: account as `0x${string}`,
+ spender: getBundlerV2(groupedPosition.chainId),
+ token: groupedPosition.loanAssetAddress as `0x${string}`,
+ refetchInterval: 10000,
+ chainId: groupedPosition.chainId,
+ tokenSymbol: groupedPosition.loanAsset,
+ amount: totalAmount,
+ });
+
+ const addRebalanceAction = useCallback((action: RebalanceAction) => {
+ setRebalanceActions((prev) => [...prev, action]);
+ }, []);
+
+ const removeRebalanceAction = useCallback((index: number) => {
+ setRebalanceActions((prev) => prev.filter((_, i) => i !== index));
+ }, []);
+
+ const { sendTransactionAsync } = useTransactionWithToast({
+ toastId: 'rebalance',
+ pendingText: 'Rebalancing positions',
+ successText: 'Positions rebalanced successfully',
+ errorText: 'Failed to rebalance positions',
+ chainId: groupedPosition.chainId,
+ });
+
+ const executeRebalance = useCallback(async () => {
+ if (!account) {
+ return;
+ }
+ setIsConfirming(true);
+ const transactions: `0x${string}`[] = [];
+
+ try {
+ // Step 1: Authorize Permit2 if needed
+ setCurrentStep('approve');
+ if (!permit2Authorized) {
+ await authorizePermit2();
+
+ await new Promise((resolve) => setTimeout(resolve, 800));
+ }
+
+ // Step 2: Sign and authorize bundler if needed
+ setCurrentStep('authorize');
+ if (isAuthorized === false) {
+ const domain = {
+ chainId: groupedPosition.chainId,
+ verifyingContract: MORPHO as Address,
+ };
+
+ const types = {
+ Authorization: [
+ { name: 'authorizer', type: 'address' },
+ { name: 'authorized', type: 'address' },
+ { name: 'isAuthorized', type: 'bool' },
+ { name: 'nonce', type: 'uint256' },
+ { name: 'deadline', type: 'uint256' },
+ ],
+ };
+
+ const deadline = Math.floor(Date.now() / 1000) + 3600;
+
+ const value = {
+ authorizer: account,
+ authorized: bundlerAddress,
+ isAuthorized: true,
+ nonce: nonce,
+ deadline: BigInt(deadline),
+ };
+
+ let signatureRaw;
+ try {
+ signatureRaw = await signTypedDataAsync({
+ domain,
+ types,
+ primaryType: 'Authorization',
+ message: value,
+ });
+ } catch (error) {
+ toast.error('Signature request was rejected or failed. Please try again.');
+ return;
+ }
+ const signature = parseSignature(signatureRaw);
+
+ const authorizationTx = encodeFunctionData({
+ abi: morphoBundlerAbi,
+ functionName: 'morphoSetAuthorizationWithSig',
+ args: [
+ {
+ authorizer: account as Address,
+ authorized: bundlerAddress,
+ isAuthorized: true,
+ nonce: BigInt(nonce ?? 0),
+ deadline: BigInt(deadline),
+ },
+ {
+ v: Number(signature.v),
+ r: signature.r,
+ s: signature.s,
+ },
+ false,
+ ],
+ });
+
+ transactions.push(authorizationTx);
+
+ // wait 800ms to avoid rabby wallet issue
+ await new Promise((resolve) => setTimeout(resolve, 800));
+ }
+
+ // Step 3: Sign permit for USDC
+ setCurrentStep('sign');
+ const { sigs, permitSingle } = await signForBundlers();
+ console.log('Signed for bundlers:', { sigs, permitSingle });
+
+ const permitTx = encodeFunctionData({
+ abi: morphoBundlerAbi,
+ functionName: 'approve2',
+ args: [permitSingle, sigs, false],
+ });
+ const transferFromTx = encodeFunctionData({
+ abi: morphoBundlerAbi,
+ functionName: 'transferFrom2',
+ args: [groupedPosition.loanAssetAddress as Address, totalAmount],
+ });
+
+ // don't push the transferFromTx to the array, do it after all withdrawals. Here we only dealt with permit
+ transactions.push(permitTx);
+
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ // Step 4: Append rebalance actions and generate tx
+ setCurrentStep('execute');
+
+ const withdrawTxs: `0x${string}`[] = [];
+ const supplyTxs: `0x${string}`[] = [];
+
+ // Group actions by market
+ const groupedWithdraws: Record = {};
+ const groupedSupplies: Record = {};
+
+ rebalanceActions.forEach((action) => {
+ const withdrawKey = action.fromMarket.uniqueKey;
+ const supplyKey = action.toMarket.uniqueKey;
+
+ if (!groupedWithdraws[withdrawKey]) groupedWithdraws[withdrawKey] = [];
+ if (!groupedSupplies[supplyKey]) groupedSupplies[supplyKey] = [];
+
+ groupedWithdraws[withdrawKey].push(action);
+ groupedSupplies[supplyKey].push(action);
+ });
+
+ // Generate batched withdraw transactions
+ Object.values(groupedWithdraws).forEach((actions) => {
+ const batchAmount = actions.reduce((sum, action) => sum + BigInt(action.amount), BigInt(0));
+ const market = actions[0].fromMarket;
+
+ const withdrawTx = encodeFunctionData({
+ abi: morphoBundlerAbi,
+ functionName: 'morphoWithdraw',
+ args: [
+ {
+ loanToken: market.loanToken as Address,
+ collateralToken: market.collateralToken as Address,
+ oracle: market.oracle as Address,
+ irm: market.irm as Address,
+ lltv: BigInt(market.lltv),
+ },
+ batchAmount, // assets
+ BigInt(0), // shares
+ maxUint256, // slippageAmount => max share burned
+ account, // receiver
+ ],
+ });
+
+ withdrawTxs.push(withdrawTx);
+ });
+
+ // Generate batched supply transactions
+ Object.values(groupedSupplies).forEach((actions) => {
+ const bachedAmount = actions.reduce(
+ (sum, action) => sum + BigInt(action.amount),
+ BigInt(0),
+ );
+ const market = actions[0].toMarket;
+
+ const supplyTx = encodeFunctionData({
+ abi: morphoBundlerAbi,
+ functionName: 'morphoSupply',
+ args: [
+ {
+ loanToken: market.loanToken as Address,
+ collateralToken: market.collateralToken as Address,
+ oracle: market.oracle as Address,
+ irm: market.irm as Address,
+ lltv: BigInt(market.lltv),
+ },
+ bachedAmount,
+ BigInt(0),
+ BigInt(0), // slippageAmount => min share minted
+ account,
+ '0x',
+ ],
+ });
+
+ supplyTxs.push(supplyTx);
+ });
+
+ // Reorder transactions
+ transactions.push(...withdrawTxs);
+ transactions.push(transferFromTx);
+ transactions.push(...supplyTxs);
+
+ // Execute all transactions
+ const multicallTx = encodeFunctionData({
+ abi: morphoBundlerAbi,
+ functionName: 'multicall',
+ args: [transactions],
+ });
+
+ await sendTransactionAsync({
+ account,
+ to: bundlerAddress,
+ data: multicallTx,
+ chainId: groupedPosition.chainId,
+ });
+
+ setRebalanceActions([]);
+ } catch (error) {
+ console.error('Error during rebalance:', error);
+ toast.error('An error occurred during rebalance. Please try again.');
+ throw error;
+ } finally {
+ setIsConfirming(false);
+ setCurrentStep('idle');
+ }
+ }, [
+ account,
+ permit2Authorized,
+ authorizePermit2,
+ signForBundlers,
+ isAuthorized,
+ nonce,
+ bundlerAddress,
+ groupedPosition.chainId,
+ signTypedDataAsync,
+ rebalanceActions,
+ sendTransactionAsync,
+ groupedPosition.loanAssetAddress,
+ totalAmount,
+ ]);
+
+ return {
+ rebalanceActions,
+ addRebalanceAction,
+ removeRebalanceAction,
+ executeRebalance,
+ isConfirming,
+ currentStep,
+ isAuthorized: permit2Authorized,
+ };
+};
diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts
index fb3d6ea6..b3164dd0 100644
--- a/src/hooks/useUserPositions.ts
+++ b/src/hooks/useUserPositions.ts
@@ -52,6 +52,8 @@ const query = `query getUserMarketPositions(
liquidityAssets
supplyAssetsUsd
supplyAssets
+ borrowAssets
+ borrowAssetsUsd
rewards {
yearlySupplyTokens
asset {
@@ -60,6 +62,26 @@ const query = `query getUserMarketPositions(
spotPriceEth
}
}
+ utilization
+ }
+ oracleFeed {
+ baseFeedOneAddress
+ baseFeedOneDescription
+ baseFeedTwoAddress
+ baseFeedTwoDescription
+ quoteFeedOneAddress
+ quoteFeedOneDescription
+ quoteFeedTwoAddress
+ quoteFeedTwoDescription
+ baseVault
+ baseVaultDescription
+ baseVaultVendor
+ quoteVault
+ quoteVaultDescription
+ quoteVaultVendor
+ }
+ oracleInfo {
+ type
}
}
}
diff --git a/src/utils/types.ts b/src/utils/types.ts
index edc28b76..487a3384 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -10,6 +10,10 @@ export type MarketPosition = {
uniqueKey: string;
lltv: string;
oracleAddress: string;
+ oracleFeed?: OracleFeedsInfo;
+ oracleInfo: {
+ type: string;
+ };
irmAddress: string;
morphoBlue: {
id: string;
@@ -41,6 +45,8 @@ export type MarketPosition = {
liquidityAssets: string;
supplyAssets: string;
supplyAssetsUsd: number;
+ borrowAssets: string;
+ borrowAssetsUsd: number;
rewards: {
yearlySupplyTokens: string;
asset: {
@@ -49,6 +55,7 @@ export type MarketPosition = {
spotPriceEth: string | null;
};
}[];
+ utilization: number;
};
};
};
@@ -195,3 +202,40 @@ export type UniformRewardType = {
// Combined RewardResponseType
export type RewardResponseType = MarketProgramType | UniformRewardType;
+
+export type RebalanceAction = {
+ fromMarket: {
+ loanToken: string;
+ collateralToken: string;
+ oracle: string;
+ irm: string;
+ lltv: string;
+ uniqueKey: string;
+ };
+ toMarket: {
+ loanToken: string;
+ collateralToken: string;
+ oracle: string;
+ irm: string;
+ lltv: string;
+ uniqueKey: string;
+ };
+ amount: bigint;
+};
+
+export type GroupedPosition = {
+ loanAsset: string;
+ loanAssetAddress: string;
+ loanAssetDecimals: number;
+ chainId: number;
+ totalSupply: number;
+ totalWeightedApy: number;
+ collaterals: { address: string; symbol: string | undefined; amount: number }[];
+ markets: MarketPosition[];
+ processedCollaterals: {
+ address: string;
+ symbol: string | undefined;
+ amount: number;
+ percentage: number;
+ }[];
+};