Optimize your {groupedPosition.loanAsset} lending strategy by redistributing funds
across markets, add "Rebalance" actions to fine-tune your portfolio.
@@ -297,9 +297,7 @@ export function RebalanceModal({
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"
>
- {needSwitchChain
- ? 'Switch Network & Execute'
- : 'Execute Rebalance'}
+ {needSwitchChain ? 'Switch Network & Execute' : 'Execute Rebalance'}
diff --git a/app/positions/components/SmartOnboarding.tsx b/app/positions/components/SmartOnboarding.tsx
new file mode 100644
index 00000000..64226b9c
--- /dev/null
+++ b/app/positions/components/SmartOnboarding.tsx
@@ -0,0 +1,14 @@
+import { AssetSelection } from './onboarding/AssetSelection';
+import { OnboardingProvider } from './onboarding/OnboardingContext';
+import { RiskSelection } from './onboarding/RiskSelection';
+
+export function SmartOnboarding() {
+ return (
+
+ );
+}
diff --git a/app/positions/components/onboarding/AssetSelection.tsx b/app/positions/components/onboarding/AssetSelection.tsx
new file mode 100644
index 00000000..486d84d8
--- /dev/null
+++ b/app/positions/components/onboarding/AssetSelection.tsx
@@ -0,0 +1,170 @@
+import { useMemo } from 'react';
+import { Button } from '@nextui-org/react';
+import { motion } from 'framer-motion';
+import Image from 'next/image';
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+import { useMarkets } from '@/hooks/useMarkets';
+import { useUserBalances } from '@/hooks/useUserBalances';
+import { formatBalance } from '@/utils/balance';
+import { getNetworkImg, getNetworkName } from '@/utils/networks';
+import { useOnboarding } from './OnboardingContext';
+import { TokenWithMarkets } from './types';
+
+function NetworkIcon({ networkId }: { networkId: number }) {
+ const url = getNetworkImg(networkId);
+ return (
+
+ );
+}
+
+export function AssetSelection() {
+ const { balances, loading: balancesLoading } = useUserBalances();
+ const { markets, loading: marketsLoading } = useMarkets();
+ const { setSelectedToken, setSelectedMarkets } = useOnboarding();
+ const router = useRouter();
+
+ const tokensWithMarkets = useMemo(() => {
+ if (!balances || !markets) return [];
+
+ const result: TokenWithMarkets[] = [];
+
+ balances.forEach((balance) => {
+ // Filter markets for this specific token and network
+ const relevantMarkets = markets.filter(
+ (market) =>
+ market.morphoBlue.chain.id === balance.chainId &&
+ market.loanAsset.address.toLowerCase() === balance.address.toLowerCase(),
+ );
+
+ if (relevantMarkets.length === 0) return;
+
+ // Calculate min and max APY
+ const apys = relevantMarkets.map((market) => market.state.supplyApy);
+ const minApy = Math.min(...apys);
+ const maxApy = Math.max(...apys);
+
+ // Get network name
+ const network = balance.chainId;
+
+ result.push({
+ symbol: balance.symbol,
+ markets: relevantMarkets,
+ minApy,
+ maxApy,
+ logoURI: balance.logoURI,
+ decimals: balance.decimals,
+ network,
+ address: balance.address,
+ balance: balance.balance,
+ });
+ });
+
+ return result;
+ }, [balances, markets]);
+
+ const handleTokenSelect = (token: TokenWithMarkets) => {
+ setSelectedToken(token);
+ setSelectedMarkets([]); // Reset selected markets when changing token
+ router.push('/positions/onboarding?step=risk-selection');
+ };
+
+ if (balancesLoading || marketsLoading) {
+ return (
+
+
+
Select an Asset
+
Choose which asset you want to supply
+
+
Loading...
+
+ );
+ }
+
+ return (
+
+
+
Select an Asset
+
Choose which asset you want to supply
+
+
+ {tokensWithMarkets.length === 0 ? (
+
+
No assets available
+
+ You need to have some assets in your wallet to supply
+
+
+
+
+
+ ) : (
+
+ {tokensWithMarkets.map((token) => (
+
handleTokenSelect(token)}
+ className="group relative flex items-start gap-4 rounded-lg border border-gray-200 bg-white p-4 text-left transition-all duration-300 hover:border-primary hover:shadow-lg dark:border-gray-700 dark:bg-gray-800/50 dark:hover:bg-gray-800"
+ whileHover={{ scale: 1.02 }}
+ transition={{ type: 'spring', stiffness: 300, damping: 20 }}
+ >
+
+ {token.logoURI && (
+
+ )}
+
+
+
+
+
+ {token.symbol}
+
+
+
+
+ {getNetworkName(token.network)}
+
+
+
+
+
+
+
+ Balance: {formatBalance(token.balance, token.decimals)} {token.symbol}
+
+
+
+
+ {token.markets.length} market{token.markets.length !== 1 ? 's' : ''}
+
+
•
+
+ {(token.minApy * 100).toFixed(2)}% - {(token.maxApy * 100).toFixed(2)}% APY
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/app/positions/components/onboarding/OnboardingContent.tsx b/app/positions/components/onboarding/OnboardingContent.tsx
new file mode 100644
index 00000000..15fbc870
--- /dev/null
+++ b/app/positions/components/onboarding/OnboardingContent.tsx
@@ -0,0 +1,40 @@
+'use client';
+
+import { useSearchParams } from 'next/navigation';
+import Header from '@/components/layout/header/Header';
+import { AssetSelection } from './AssetSelection';
+import { OnboardingProvider } from './OnboardingContext';
+import { RiskSelection } from './RiskSelection';
+import { SetupPositions } from './SetupPositions';
+import { SuccessPage } from './SuccessPage';
+
+export function OnboardingContent() {
+ const searchParams = useSearchParams();
+ const step = searchParams.get('step') ?? 'asset-selection';
+
+ const renderStep = () => {
+ switch (step) {
+ case 'asset-selection':
+ return
;
+ case 'risk-selection':
+ return
;
+ case 'setup':
+ return
;
+ case 'success':
+ return
;
+ default:
+ return
;
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/app/positions/components/onboarding/OnboardingContext.tsx b/app/positions/components/onboarding/OnboardingContext.tsx
new file mode 100644
index 00000000..750d3605
--- /dev/null
+++ b/app/positions/components/onboarding/OnboardingContext.tsx
@@ -0,0 +1,72 @@
+import { createContext, useContext, useState, useMemo } from 'react';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { Market } from '@/utils/types';
+import { TokenWithMarkets } from './types';
+
+type OnboardingStep = 'asset-selection' | 'risk-selection' | 'setup' | 'success';
+
+type OnboardingContextType = {
+ selectedToken: TokenWithMarkets | null;
+ setSelectedToken: (token: TokenWithMarkets | null) => void;
+ selectedMarkets: Market[];
+ setSelectedMarkets: (markets: Market[]) => void;
+ step: OnboardingStep;
+ setStep: (step: OnboardingStep) => void;
+};
+
+const OnboardingContext = createContext
(null);
+
+export function OnboardingProvider({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const currentStep = (searchParams.get('step') as OnboardingStep) || 'asset-selection';
+
+ const [selectedToken, setSelectedToken] = useState(null);
+ const [selectedMarkets, setSelectedMarkets] = useState([]);
+
+ const setStep = (newStep: OnboardingStep) => {
+ const params = new URLSearchParams(searchParams.toString());
+ params.set('step', newStep);
+ router.push(`/positions/onboarding?${params.toString()}`);
+ };
+
+ const contextValue = useMemo(
+ () => ({
+ selectedToken,
+ setSelectedToken: (token: TokenWithMarkets | null) => {
+ setSelectedToken(token);
+ // Reset markets when token changes
+ setSelectedMarkets([]);
+ },
+ selectedMarkets,
+ setSelectedMarkets: (markets: Market[]) => {
+ setSelectedMarkets(markets);
+ },
+ step: currentStep,
+ setStep: (newStep: OnboardingStep) => {
+ // Validate step transitions
+ if (newStep !== 'asset-selection' && !selectedToken) {
+ throw new Error('Token must be selected before proceeding');
+ }
+ if (newStep === 'setup' && selectedMarkets.length === 0) {
+ throw new Error('Markets must be selected before setup');
+ }
+ if (newStep === 'success' && !selectedToken) {
+ throw new Error('Token must be selected before showing success');
+ }
+ setStep(newStep);
+ },
+ }),
+ [selectedToken, selectedMarkets, currentStep],
+ );
+
+ return {children};
+}
+
+export function useOnboarding() {
+ const context = useContext(OnboardingContext);
+ if (!context) {
+ throw new Error('useOnboarding must be used within an OnboardingProvider');
+ }
+ return context;
+}
diff --git a/app/positions/components/onboarding/RiskSelection.tsx b/app/positions/components/onboarding/RiskSelection.tsx
new file mode 100644
index 00000000..4cc1a7db
--- /dev/null
+++ b/app/positions/components/onboarding/RiskSelection.tsx
@@ -0,0 +1,285 @@
+import { useMemo, useState } from 'react';
+import { Button } from '@nextui-org/react';
+import Image from 'next/image';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { formatUnits } from 'viem';
+import OracleVendorBadge from '@/components/OracleVendorBadge';
+import { formatReadable } from '@/utils/balance';
+import { getAssetURL } from '@/utils/external';
+import { OracleVendors, parseOracleVendors } from '@/utils/oracle';
+import { findToken, getUniqueTokens } from '@/utils/tokens';
+import { Market } from '@/utils/types';
+import AssetFilter from 'app/markets/components/AssetFilter';
+import OracleFilter from 'app/markets/components/OracleFilter';
+import {
+ MarketDebtIndicator,
+ MarketAssetIndicator,
+ MarketOracleIndicator,
+} from 'app/markets/components/RiskIndicator';
+import { useOnboarding } from './OnboardingContext';
+
+export function RiskSelection() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const { selectedToken, setSelectedMarkets } = useOnboarding();
+ const [selectedCollaterals, setSelectedCollaterals] = useState([]);
+ const [selectedOracles, setSelectedOracles] = useState([]);
+ const [selectedMarkets, setSelectedMarketsLocal] = useState>(new Set());
+
+ const collateralTokens = useMemo(() => {
+ if (!selectedToken?.markets) return [];
+ const tokens = selectedToken.markets.map((market) => ({
+ address: market.collateralAsset.address,
+ chainId: market.morphoBlue.chain.id,
+ }));
+ return getUniqueTokens(tokens);
+ }, [selectedToken]);
+
+ // Filter markets based on selected collaterals and oracles
+ const filteredMarkets = useMemo(() => {
+ if (!selectedToken?.markets) return [];
+
+ return selectedToken.markets
+ .filter((market) => {
+ // Skip markets without known collateral
+ const collateralToken = findToken(
+ market.collateralAsset.address,
+ market.morphoBlue.chain.id,
+ );
+ if (!collateralToken) return false;
+
+ // Check if collateral is selected (if any are selected)
+ if (selectedCollaterals.length > 0) {
+ const tokenKey = `${market.collateralAsset.address.toLowerCase()}-${
+ market.morphoBlue.chain.id
+ }`;
+ if (!selectedCollaterals.some((key) => key.split('|').includes(tokenKey))) return false;
+ }
+
+ // Check if oracle is selected (if any are selected)
+ if (selectedOracles.length > 0) {
+ const { vendors } = parseOracleVendors(market.oracle.data);
+ // Check if all vendors are selected
+ if (!vendors.every((vendor) => selectedOracles.includes(vendor))) return false;
+ }
+
+ return true;
+ })
+ .sort((a, b) => {
+ const aAssets = Number(a.state.supplyAssets) || 0;
+ const bAssets = Number(b.state.supplyAssets) || 0;
+ return bAssets - aAssets;
+ });
+ }, [selectedToken, selectedCollaterals, selectedOracles]);
+
+ const handleNext = () => {
+ if (selectedMarkets.size > 0) {
+ const selectedMarketsArray = Array.from(selectedMarkets)
+ .map((key) => filteredMarkets.find((m) => m.uniqueKey === key))
+ .filter((m): m is Market => m !== undefined);
+
+ setSelectedMarkets(selectedMarketsArray);
+ router.push('/positions/onboarding?step=setup');
+ }
+ };
+
+ const handleMarketDetails = (market: Market, e: React.MouseEvent) => {
+ e.stopPropagation();
+ const currentParams = searchParams.toString();
+ const marketPath = `/market/${market.morphoBlue.chain.id}/${market.uniqueKey}`;
+ const targetPath = currentParams ? `${marketPath}?${currentParams}` : marketPath;
+ router.push(targetPath);
+ };
+
+ const toggleMarketSelection = (market: Market) => {
+ const newSelection = new Set(selectedMarkets);
+ if (selectedMarkets.has(market.uniqueKey)) {
+ newSelection.delete(market.uniqueKey);
+ } else {
+ newSelection.add(market.uniqueKey);
+ }
+ setSelectedMarketsLocal(newSelection);
+ };
+
+ return (
+
+
+
Select Your Risk Preference
+
Choose which assets and oracles you want to trust
+
+
+ {/* Input Section */}
+
+
+
+
Choose markets you want to trust
+
+
+ {/* Markets Table */}
+
+
+
+
+
+ | Market |
+ Market Params |
+ Oracle |
+ LLTV |
+ Supply APY |
+ Total Supply |
+ Utilization |
+ Actions |
+
+
+
+ {filteredMarkets.map((market) => {
+ const collateralToken = findToken(
+ market.collateralAsset.address,
+ market.morphoBlue.chain.id,
+ );
+ if (!collateralToken) return null;
+
+ const isSelected = selectedMarkets.has(market.uniqueKey);
+ const { vendors } = parseOracleVendors(market.oracle.data);
+
+ return (
+ toggleMarketSelection(market)}
+ className={`cursor-pointer transition-all duration-200 ease-in-out hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800 ${
+ isSelected ? 'bg-primary-50 dark:bg-primary-900/20' : ''
+ }`}
+ >
+
+
+ {collateralToken?.img && (
+
+
+
+ )}
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+ {vendors.map((vendor) => (
+
+ ))}
+
+ |
+
+ {formatUnits(BigInt(market.lltv), 16)}%
+ |
+
+ {formatReadable(market.state.supplyApy * 100)}%
+ |
+
+ {formatReadable(
+ Number(
+ formatUnits(BigInt(market.state.supplyAssets), market.loanAsset.decimals),
+ ),
+ )}{' '}
+ {market.loanAsset.symbol}
+ |
+
+ {formatReadable(market.state.utilization * 100)}%
+ |
+
+
+ |
+
+ );
+ })}
+
+
+
+
+
+ {/* Navigation */}
+
+
+
+
+
+ );
+}
diff --git a/app/positions/components/onboarding/SetupPositions.tsx b/app/positions/components/onboarding/SetupPositions.tsx
new file mode 100644
index 00000000..e800f165
--- /dev/null
+++ b/app/positions/components/onboarding/SetupPositions.tsx
@@ -0,0 +1,469 @@
+import { useState, useEffect, useMemo, useCallback } from 'react';
+import { Button, Slider } from '@nextui-org/react';
+import { LockClosedIcon, LockOpen1Icon } from '@radix-ui/react-icons';
+import Image from 'next/image';
+import { useRouter } from 'next/navigation';
+import { formatUnits, parseUnits } from 'viem';
+import OracleVendorBadge from '@/components/OracleVendorBadge';
+import { SupplyProcessModal } from '@/components/SupplyProcessModal';
+import { useLocalStorage } from '@/hooks/useLocalStorage';
+import { useMultiMarketSupply } from '@/hooks/useMultiMarketSupply';
+import { useUserBalances } from '@/hooks/useUserBalances';
+import { formatBalance, formatReadable } from '@/utils/balance';
+import { parseOracleVendors } from '@/utils/oracle';
+import { findToken } from '@/utils/tokens';
+import { useOnboarding } from './OnboardingContext';
+
+export function SetupPositions() {
+ const router = useRouter();
+ const { selectedToken, selectedMarkets } = useOnboarding();
+ const { balances } = useUserBalances();
+ const [useEth] = useLocalStorage('useEth', false);
+ const [usePermit2Setting] = useLocalStorage('usePermit2', true);
+ const [totalAmount, setTotalAmount] = useState('');
+ const [amounts, setAmounts] = useState>({});
+ const [percentages, setPercentages] = useState>({});
+ const [lockedAmounts, setLockedAmounts] = useState>(new Set());
+ const [error, setError] = useState(null);
+ const [isSupplying, setIsSupplying] = useState(false);
+
+ // Redirect if no token selected
+ useEffect(() => {
+ if (!selectedToken) {
+ router.push('/positions/onboarding?step=asset-selection');
+ return;
+ }
+ if (!selectedMarkets || selectedMarkets.length === 0) {
+ router.push('/positions/onboarding?step=risk-selection');
+ }
+ }, [router, selectedToken, selectedMarkets]);
+
+ // Compute token balance and decimals
+ const tokenBalance = useMemo(() => {
+ if (!selectedToken) return 0n;
+ return BigInt(
+ balances.find((b) => b.address.toLowerCase() === selectedToken.address.toLowerCase())
+ ?.balance ?? '0',
+ );
+ }, [balances, selectedToken]);
+
+ const tokenDecimals = useMemo(() => selectedToken?.decimals ?? 0, [selectedToken]);
+
+ // Initialize percentages evenly
+ useEffect(() => {
+ if (selectedMarkets.length > 0) {
+ const evenPercentage = 100 / selectedMarkets.length;
+ const initialPercentages = selectedMarkets.reduce(
+ (acc, market) => {
+ acc[market.uniqueKey] = evenPercentage;
+ return acc;
+ },
+ {} as Record,
+ );
+ setPercentages(initialPercentages);
+ }
+ }, [selectedMarkets]);
+
+ // Update amounts when total amount or percentages change
+ useEffect(() => {
+ if (totalAmount && Object.keys(percentages).length > 0) {
+ const newAmounts = Object.entries(percentages).reduce(
+ (acc, [key, percentage]) => {
+ acc[key] = ((percentage / 100) * Number(totalAmount)).toFixed(tokenDecimals);
+ return acc;
+ },
+ {} as Record,
+ );
+ setAmounts(newAmounts);
+ }
+ }, [totalAmount, percentages, tokenDecimals]);
+
+ const handleTotalAmountChange = (value: string) => {
+ // Remove any non-numeric characters except decimal point
+ const cleanValue = value.replace(/[^0-9.]/g, '');
+
+ // Ensure only one decimal point
+ const parts = cleanValue.split('.');
+ if (parts.length > 2) return;
+
+ // Limit decimal places to token decimals
+ if (parts[1] && parts[1].length > tokenDecimals) return;
+
+ try {
+ // Validate the new amount can be converted to BigInt
+ parseUnits(cleanValue || '0', tokenDecimals);
+ setTotalAmount(cleanValue);
+ } catch (e) {
+ setError('Invalid amount');
+ }
+ };
+
+ const toggleLockAmount = useCallback(
+ (marketKey: string) => {
+ const newLockedAmounts = new Set(lockedAmounts);
+ if (lockedAmounts.has(marketKey)) {
+ newLockedAmounts.delete(marketKey);
+ } else {
+ newLockedAmounts.add(marketKey);
+ }
+ setLockedAmounts(newLockedAmounts);
+ },
+ [lockedAmounts],
+ );
+
+ const handlePercentageChange = useCallback(
+ (marketKey: string, newPercentage: number) => {
+ // If the input is invalid (NaN), set it to 0
+ if (Number.isNaN(newPercentage)) {
+ newPercentage = 0;
+ }
+
+ const market = selectedMarkets.find((m) => m.uniqueKey === marketKey);
+ if (!market) return;
+
+ const lockedMarkets = selectedMarkets.filter(
+ (m) => m.uniqueKey !== marketKey && lockedAmounts.has(m.uniqueKey),
+ );
+ const unlockedMarkets = selectedMarkets.filter(
+ (m) => m.uniqueKey !== marketKey && !lockedAmounts.has(m.uniqueKey),
+ );
+
+ // Calculate total locked percentage
+ const totalLockedPercentage = lockedMarkets.reduce(
+ (sum, m) => sum + (percentages[m.uniqueKey] || 0),
+ 0,
+ );
+
+ // Ensure we don't exceed 100% - totalLockedPercentage
+ const maxAllowedPercentage = 100 - totalLockedPercentage;
+ newPercentage = Math.min(newPercentage, maxAllowedPercentage);
+
+ // Calculate remaining percentage for unlocked markets
+ const remainingPercentage = 100 - totalLockedPercentage - newPercentage;
+
+ // Distribute remaining percentage among unlocked markets proportionally
+ const newPercentages = { ...percentages };
+ newPercentages[marketKey] = newPercentage;
+
+ if (unlockedMarkets.length > 0 && remainingPercentage > 0) {
+ const currentUnlockedTotal = unlockedMarkets.reduce(
+ (sum, m) => sum + (percentages[m.uniqueKey] || 0),
+ 0,
+ );
+
+ unlockedMarkets.forEach((m) => {
+ const currentPct = percentages[m.uniqueKey] || 0;
+ const proportion =
+ currentUnlockedTotal === 0
+ ? 1 / unlockedMarkets.length
+ : currentPct / currentUnlockedTotal;
+ newPercentages[m.uniqueKey] = remainingPercentage * proportion;
+ });
+ }
+
+ setPercentages(newPercentages);
+ },
+ [percentages, selectedMarkets, lockedAmounts],
+ );
+
+ const handleAmountChange = useCallback(
+ (marketKey: string, value: string) => {
+ if (!totalAmount) return;
+
+ // Remove any non-numeric characters except decimal point
+ const cleanValue = value.replace(/[^0-9.]/g, '');
+
+ // Ensure only one decimal point
+ const parts = cleanValue.split('.');
+ if (parts.length > 2) return;
+
+ // Limit decimal places to token decimals
+ if (parts[1] && parts[1].length > tokenDecimals) return;
+
+ try {
+ // Validate the new amount can be converted to BigInt
+ parseUnits(cleanValue || '0', tokenDecimals);
+
+ const newAmount = Number(cleanValue);
+ const percentage = (newAmount / Number(totalAmount)) * 100;
+
+ // Update this market's percentage
+ handlePercentageChange(marketKey, percentage);
+ } catch (e) {
+ // If conversion fails, don't update the state
+ console.warn(`Invalid amount format: ${cleanValue}`);
+ return;
+ }
+ },
+ [totalAmount, tokenDecimals, handlePercentageChange],
+ );
+
+ const supplies = useMemo(() => {
+ if (!selectedMarkets || !amounts || !tokenDecimals) return [];
+
+ return selectedMarkets
+ .map((market) => {
+ const amount = parseUnits(amounts[market.uniqueKey] || '0', tokenDecimals);
+ return {
+ market,
+ amount,
+ };
+ })
+ .filter((supply) => supply.amount > 0n);
+ }, [selectedMarkets, amounts, tokenDecimals]);
+
+ const {
+ currentStep,
+ showProcessModal,
+ setShowProcessModal,
+ isLoadingPermit2,
+ approveAndSupply,
+ supplyPending,
+ } = useMultiMarketSupply(selectedToken!, supplies, useEth, usePermit2Setting);
+
+ const handleSupply = async () => {
+ if (isSupplying) return;
+ setIsSupplying(true);
+
+ try {
+ const success = await approveAndSupply();
+ if (success) {
+ router.push('/positions/onboarding?step=success');
+ }
+ } catch (supplyError) {
+ console.error('Supply failed:', supplyError);
+ // Error toast is already shown in useMultiMarketSupply
+ } finally {
+ setIsSupplying(false);
+ }
+ };
+
+ if (!selectedToken || !selectedMarkets || selectedMarkets.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+
Setup Your Positions
+
+ Choose how much {selectedToken.symbol} you want to supply in total and distribute it
+ across markets
+
+
+
+ {/* Total Amount Section */}
+
+
+
+
+ handleTotalAmountChange(e.target.value)}
+ placeholder="0.0"
+ className="w-full rounded border border-gray-200 bg-white px-3 py-2 pr-20 font-mono dark:border-gray-700 dark:bg-gray-800"
+ />
+
+
+
+
+
+
+ Wallet Balance
+
+
+
+ {formatBalance(tokenBalance, tokenDecimals)} {selectedToken.symbol}
+
+
+
+
+
+
+ {/* Markets Distribution */}
+
+
+
+
+
+ | Market ID |
+ Collateral |
+ Market Params |
+ Supply APY |
+ Distribution |
+
+
+
+ {selectedMarkets.map((market) => {
+ const collateralToken = findToken(
+ market.collateralAsset.address,
+ market.morphoBlue.chain.id,
+ );
+ if (!collateralToken) return null;
+
+ const { vendors } = parseOracleVendors(market.oracle.data);
+ const currentPercentage = percentages[market.uniqueKey] ?? 0;
+ const isLocked = lockedAmounts.has(market.uniqueKey);
+
+ return (
+
+ |
+
+ {market.uniqueKey.slice(2, 8)}
+
+ |
+
+
+ {collateralToken?.img && (
+
+
+
+ )}
+
+ {market.collateralAsset.symbol}
+ as collateral
+
+
+ |
+
+
+
+ {vendors.map((vendor) => (
+
+ ))}
+
+
+ {formatUnits(BigInt(market.lltv), 16)}% LTV
+
+
+ |
+
+ {formatReadable(market.state.supplyApy * 100)}%
+ |
+
+
+
+
+
+ handlePercentageChange(market.uniqueKey, Number(value))
+ }
+ className="max-w-md"
+ classNames={{
+ base: 'max-w-md gap-3',
+ track: 'bg-default-500/30',
+ thumb: 'bg-primary',
+ }}
+ isDisabled={isLocked}
+ />
+
+
+
+
+ handleAmountChange(market.uniqueKey, e.target.value)
+ }
+ placeholder="0.0"
+ className="bg-hovered focus:border-monarch-orange h-8 w-full rounded p-2 text-right font-mono focus:outline-none"
+ disabled={isLocked}
+ />
+
+
+ {Math.round(currentPercentage)}%
+
+
+
+
+
+ |
+
+ );
+ })}
+
+
+
+
+
+ {error &&
{error}
}
+
+ {/* Process Modal */}
+ {showProcessModal && (
+
setShowProcessModal(false)}
+ tokenSymbol={selectedToken.symbol}
+ useEth={useEth}
+ usePermit2={usePermit2Setting}
+ />
+ )}
+
+ {/* Navigation */}
+
+
+
+
+
+ );
+}
diff --git a/app/positions/components/onboarding/SmartOnboarding.tsx b/app/positions/components/onboarding/SmartOnboarding.tsx
new file mode 100644
index 00000000..3085b4de
--- /dev/null
+++ b/app/positions/components/onboarding/SmartOnboarding.tsx
@@ -0,0 +1,22 @@
+import { AssetSelection } from './AssetSelection';
+import { OnboardingProvider, useOnboarding } from './OnboardingContext';
+import { RiskSelection } from './RiskSelection';
+
+function OnboardingContent() {
+ const { step } = useOnboarding();
+
+ return (
+
+ {step === 'asset-selection' &&
}
+ {step === 'risk-selection' &&
}
+
+ );
+}
+
+export function SmartOnboarding() {
+ return (
+
+
+
+ );
+}
diff --git a/app/positions/components/onboarding/SuccessPage.tsx b/app/positions/components/onboarding/SuccessPage.tsx
new file mode 100644
index 00000000..8b87ba90
--- /dev/null
+++ b/app/positions/components/onboarding/SuccessPage.tsx
@@ -0,0 +1,37 @@
+import { Button } from '@nextui-org/react';
+import Link from 'next/link';
+import { FaCheckCircle } from 'react-icons/fa';
+import { useAccount } from 'wagmi';
+import { useOnboarding } from './OnboardingContext';
+
+export function SuccessPage() {
+ const { selectedToken } = useOnboarding();
+ const { address } = useAccount();
+
+ return (
+
+
+
+
+
Success!
+
+
+ Your {selectedToken?.symbol} has been successfully supplied to Morpho Blue.
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/positions/components/onboarding/types.ts b/app/positions/components/onboarding/types.ts
new file mode 100644
index 00000000..8ef31e45
--- /dev/null
+++ b/app/positions/components/onboarding/types.ts
@@ -0,0 +1,21 @@
+import { NetworkToken } from '@/types/token';
+import { Market } from '@/utils/types';
+
+export type TokenWithMarkets = NetworkToken & {
+ markets: Market[];
+ minApy: number;
+ maxApy: number;
+ logoURI?: string;
+ balance: string;
+};
+
+export type OnboardingStep = 'asset-selection' | 'risk-selection' | 'setup';
+
+export type OnboardingContextType = {
+ step: OnboardingStep;
+ selectedToken?: TokenWithMarkets;
+ selectedMarkets: Market[];
+ setStep: (step: OnboardingStep) => void;
+ setSelectedToken: (token: TokenWithMarkets) => void;
+ setSelectedMarkets: (markets: Market[]) => void;
+};
diff --git a/app/positions/onboarding/page.tsx b/app/positions/onboarding/page.tsx
new file mode 100644
index 00000000..70b6437d
--- /dev/null
+++ b/app/positions/onboarding/page.tsx
@@ -0,0 +1,11 @@
+import { Metadata } from 'next';
+import { OnboardingContent } from '../components/onboarding/OnboardingContent';
+
+export const metadata: Metadata = {
+ title: 'New Position | Monarch',
+ description: 'Create a new position on Morpho Blue',
+};
+
+export default function OnboardingPage() {
+ return ;
+}
diff --git a/app/rewards/components/RewardContent.tsx b/app/rewards/components/RewardContent.tsx
index ab965612..f75e8200 100644
--- a/app/rewards/components/RewardContent.tsx
+++ b/app/rewards/components/RewardContent.tsx
@@ -21,7 +21,7 @@ export default function Rewards() {
const { account } = useParams<{ account: string }>();
const [activeProgram, setActiveProgram] = useState<'market' | 'uniform'>('market');
- const { loading, markets } = useMarkets();
+ const { loading, markets } = useMarkets();
const { rewards, distributions, loading: loadingRewards } = useUserRewards(account);
console.log('distributions', distributions);
diff --git a/app/settings/page.tsx b/app/settings/page.tsx
index 508de02b..a85e6db0 100644
--- a/app/settings/page.tsx
+++ b/app/settings/page.tsx
@@ -20,21 +20,23 @@ export default function SettingsPage() {
Settings
-
+
{/* Transaction Settings Section */}
Transaction Settings
-
-
+
+
Use Gasless Approvals
-
- Enable signature-based token approvals using Permit2. This bundles approvals and actions into a single transaction, saving gas.
+
+ Enable signature-based token approvals using Permit2. This bundles approvals and
+ actions into a single transaction, saving gas.
- Note: If you're using a smart contract wallet (like Safe or other multisig), you may want to disable this and use standard approvals instead.
+ Note: If you're using a smart contract wallet (like Safe or other multisig), you
+ may want to disable this and use standard approvals instead.
-
+
);
diff --git a/src/components/ButtonGroup.tsx b/src/components/ButtonGroup.tsx
index 90e2badb..7ca36287 100644
--- a/src/components/ButtonGroup.tsx
+++ b/src/components/ButtonGroup.tsx
@@ -57,7 +57,7 @@ export default function ButtonGroup({
variant = 'default',
}: ButtonGroupProps) {
return (
-
+
{options.map((option, index) => {
const isFirst = index === 0;
const isLast = index === options.length - 1;
@@ -74,8 +74,8 @@ export default function ButtonGroup({
sizeClasses[size],
// Position-based styles
- isFirst ? 'rounded-l-lg' : '-ml-px',
- isLast ? 'rounded-r-lg' : '',
+ isFirst ? 'rounded-l' : '-ml-px rounded-none',
+ isLast ? 'rounded-r' : 'rounded-none',
// Variant & State styles
variant === 'default'
diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx
index 21b688fc..5f1ddf7c 100644
--- a/src/components/Input/Input.tsx
+++ b/src/components/Input/Input.tsx
@@ -65,7 +65,7 @@ export default function Input({
type="number"
value={inputAmount}
onChange={onInputChange}
- className="bg-hovered focus:border-monarch-orange h-10 w-full rounded p-2 focus:outline-none"
+ className="bg-hovered h-10 w-full rounded p-2 focus:border-primary focus:outline-none"
/>