diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 4563d38b..ccf055bb 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -23,7 +23,8 @@ "Bash(pnpm exec tsc:*)", "Bash(npx eslint:*)", "Bash(git rm:*)", - "Bash(cat:*)" + "Bash(cat:*)", + "Bash(timeout 30 pnpm exec tsc --noEmit --pretty)" ], "deny": [] } diff --git a/app/history/components/HistoryTable.tsx b/app/history/components/HistoryTable.tsx index b83bc4b9..0f8e875c 100644 --- a/app/history/components/HistoryTable.tsx +++ b/app/history/components/HistoryTable.tsx @@ -305,7 +305,7 @@ export function HistoryTable({ account, positions, rebalancerInfos }: HistoryTab Transaction - {history.map((tx, index) => { + {history.filter(tx => tx.data.market !== undefined).map((tx, index) => { // safely cast here because we only fetch txs for unique id in "markets" const market = allMarkets.find( (m) => m.uniqueKey === tx.data.market.uniqueKey, diff --git a/app/markets/components/Pagination.tsx b/app/markets/components/Pagination.tsx index 1f428b66..d1bb035b 100644 --- a/app/markets/components/Pagination.tsx +++ b/app/markets/components/Pagination.tsx @@ -7,6 +7,7 @@ type PaginationProps = { onPageChange: (page: number) => void; entriesPerPage: number; isDataLoaded: boolean; + size?: "lg"| "md" |"sm" }; export function Pagination({ @@ -15,6 +16,7 @@ export function Pagination({ onPageChange, entriesPerPage, isDataLoaded, + size = "md" }: PaginationProps) { if (!isDataLoaded || totalPages === 0) { return null; @@ -35,7 +37,7 @@ export function Pagination({ item: 'w-8 h-8 text-small rounded-sm bg-transparent', cursor: 'bg-orange-500 text-white font-bold', }} - size="md" + size={size} /> diff --git a/app/positions/components/onboarding/MarketSelectionOnboarding.tsx b/app/positions/components/onboarding/MarketSelectionOnboarding.tsx new file mode 100644 index 00000000..b49f4c2b --- /dev/null +++ b/app/positions/components/onboarding/MarketSelectionOnboarding.tsx @@ -0,0 +1,104 @@ +import { useMemo } from 'react'; +import { Button } from '@/components/common/Button'; +import { MarketsTableWithSameLoanAsset } from '@/components/common/MarketsTableWithSameLoanAsset'; +import type { MarketWithSelection } from '@/components/common/MarketsTableWithSameLoanAsset'; +import { useTokens } from '@/components/providers/TokenProvider'; +import { useOnboarding } from './OnboardingContext'; + +export function MarketSelectionOnboarding() { + const { + selectedToken, + selectedMarkets, + setSelectedMarkets, + canGoNext, + goToNextStep, + goToPrevStep, + } = useOnboarding(); + + const { getUniqueTokens } = useTokens(); + + // Get unique collateral tokens for filter performance + const collateralTokens = useMemo(() => { + if (!selectedToken?.markets) return []; + const tokens = selectedToken.markets + .filter((market) => market?.collateralAsset?.address && market?.morphoBlue?.chain?.id) + .map((market) => ({ + address: market.collateralAsset.address, + chainId: market.morphoBlue.chain.id, + })); + return getUniqueTokens(tokens); + }, [selectedToken?.markets, getUniqueTokens]); + + // Convert markets to the format expected by the table + const marketsWithSelection: MarketWithSelection[] = useMemo(() => { + if (!selectedToken?.markets) return []; + return selectedToken.markets + .filter((market) => market && market.uniqueKey) + .map((market) => ({ + market, + isSelected: selectedMarkets.some((m) => m?.uniqueKey === market.uniqueKey), + })); + }, [selectedToken?.markets, selectedMarkets]); + + // Handle case when no token is selected yet + if (!selectedToken) { + return ( +
+

No token selected. Please go back and select a token.

+ +
+ ); + } + + const handleToggleMarket = (marketId: string) => { + if (!selectedToken.markets) return; + const market = selectedToken.markets.find((m) => m?.uniqueKey === marketId); + if (!market) return; + + if (selectedMarkets.some((m) => m?.uniqueKey === marketId)) { + setSelectedMarkets(selectedMarkets.filter((m) => m?.uniqueKey !== marketId)); + } else { + setSelectedMarkets([...selectedMarkets, market]); + } + }; + + return ( +
+ {/* Description Section */} +
+

+ Filter markets by your risk preferences and select the ones you want to use for your position. +

+
+ + {/* Markets Table Section */} +
+ +
+ + {/* Navigation - ALWAYS VISIBLE */} +
+ + +
+
+ ); +} diff --git a/app/positions/components/onboarding/Modal.tsx b/app/positions/components/onboarding/Modal.tsx index 8fc1c062..05d456e2 100644 --- a/app/positions/components/onboarding/Modal.tsx +++ b/app/positions/components/onboarding/Modal.tsx @@ -2,15 +2,15 @@ import { Modal, ModalContent, ModalHeader, Button } from '@heroui/react'; import { motion, AnimatePresence } from 'framer-motion'; import { RxCross2 } from 'react-icons/rx'; import { AssetSelection } from './AssetSelection'; +import { MarketSelectionOnboarding } from './MarketSelectionOnboarding'; import { useOnboarding } from './OnboardingContext'; import { ONBOARDING_STEPS } from './OnboardingContext'; -import { RiskSelection } from './RiskSelection'; import { SetupPositions } from './SetupPositions'; import { SuccessPage } from './SuccessPage'; const StepComponents = { 'asset-selection': AssetSelection, - 'risk-selection': RiskSelection, + 'market-selection': MarketSelectionOnboarding, setup: SetupPositions, success: SuccessPage, } as const; diff --git a/app/positions/components/onboarding/OnboardingContext.tsx b/app/positions/components/onboarding/OnboardingContext.tsx index 9bd89b07..ad499eeb 100644 --- a/app/positions/components/onboarding/OnboardingContext.tsx +++ b/app/positions/components/onboarding/OnboardingContext.tsx @@ -1,4 +1,5 @@ import { createContext, useContext, useState, useMemo, useCallback } from 'react'; +import { useUserBalancesAllNetworks } from '@/hooks/useUserBalances'; import { Market } from '@/utils/types'; import { TokenWithMarkets } from './types'; @@ -8,7 +9,7 @@ export const ONBOARDING_STEPS = [ title: 'Select Asset', description: 'Choose the asset you want to supply', }, - { id: 'risk-selection', title: 'Select Markets', description: '' }, + { id: 'market-selection', title: 'Select Markets', description: '' }, { id: 'setup', title: 'Position Setup', description: 'Configure your initial position' }, { id: 'success', title: 'Complete', description: 'Position created successfully' }, ] as const; @@ -27,6 +28,10 @@ type OnboardingContextType = { goToNextStep: () => void; goToPrevStep: () => void; resetOnboarding: () => void; + + // Shared balances across all steps + balances: { address: string; balance: string }[]; + balancesLoading: boolean; }; const OnboardingContext = createContext(null); @@ -37,13 +42,16 @@ export function OnboardingProvider({ children }: { children: React.ReactNode }) const [currentStep, setStep] = useState('asset-selection'); + // Fetch user balances once for the entire onboarding flow + const { balances, loading: balancesLoading } = useUserBalancesAllNetworks(); + const currentStepIndex = ONBOARDING_STEPS.findIndex((s) => s.id === currentStep); const canGoNext = useMemo(() => { switch (currentStep) { case 'asset-selection': return !!selectedToken; - case 'risk-selection': + case 'market-selection': return selectedMarkets.length > 0; case 'setup': return true; @@ -87,6 +95,8 @@ export function OnboardingProvider({ children }: { children: React.ReactNode }) goToNextStep, goToPrevStep, resetOnboarding, + balances, + balancesLoading, }), [ selectedToken, @@ -99,6 +109,8 @@ export function OnboardingProvider({ children }: { children: React.ReactNode }) setSelectedToken, setSelectedMarkets, setStep, + balances, + balancesLoading, ], ); diff --git a/app/positions/components/onboarding/RiskSelection.tsx b/app/positions/components/onboarding/RiskSelection.tsx deleted file mode 100644 index b344f3e9..00000000 --- a/app/positions/components/onboarding/RiskSelection.tsx +++ /dev/null @@ -1,300 +0,0 @@ -import { useMemo, useState } from 'react'; -import { ExternalLinkIcon } from '@radix-ui/react-icons'; -import { motion } from 'framer-motion'; -import { Button } from '@/components/common/Button'; -import OracleVendorBadge from '@/components/OracleVendorBadge'; -import { useTokens } from '@/components/providers/TokenProvider'; -import { formatBalance, formatReadable } from '@/utils/balance'; -import { PriceFeedVendors, parsePriceFeedVendors } from '@/utils/oracle'; -import { Market } from '@/utils/types'; -import { APYCell } from 'app/markets/components/APYBreakdownTooltip'; -import AssetFilter from 'app/markets/components/AssetFilter'; -import { TDAsset } from 'app/markets/components/MarketTableUtils'; -import OracleFilter from 'app/markets/components/OracleFilter'; -import { - MarketDebtIndicator, - MarketAssetIndicator, - MarketOracleIndicator, -} from 'app/markets/components/RiskIndicator'; -import { useOnboarding } from './OnboardingContext'; - -export function RiskSelection() { - const { - selectedToken, - selectedMarkets, - setSelectedMarkets, - canGoNext, - goToNextStep, - goToPrevStep, - } = useOnboarding(); - const [selectedCollaterals, setSelectedCollaterals] = useState([]); - const [selectedOracles, setSelectedOracles] = useState([]); - - const { findToken, getUniqueTokens } = useTokens(); - - 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 } = parsePriceFeedVendors( - market.oracle?.data, - market.morphoBlue.chain.id, - ); - - // if vendors is empty, push "unknown oracle" into list that needed to be selected - if (vendors.length === 0) { - vendors.push(PriceFeedVendors.Unknown); - } - - // 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]); - - // Check if criteria is met to show markets - const shouldShowMarkets = selectedCollaterals.length > 0; - - const handleMarketDetails = (market: Market) => { - const marketPath = `/market/${market.morphoBlue.chain.id}/${market.uniqueKey}`; - window.open(marketPath, '_blank'); - }; - - return ( -
- {/* Description Section */} -
-

- Filter markets by your risk preferences and select the ones you want to use for your position. -

-
- -
-
- -
-
- -
-
- - {/* Markets Table Section - FIXED HEIGHT */} -
- {!shouldShowMarkets ? ( -
-
-

Select your risk preferences

-

- {selectedCollaterals.length === 0 && 'Choose at least one collateral asset to view available markets'} - {selectedCollaterals.length > 0 && - selectedOracles.length === 0 && - 'Now select oracle vendors to filter markets'} -

-
-
- ) : ( - - - - - - - - - - - - - - - {filteredMarkets.map((market, index) => { - const collateralToken = findToken( - market.collateralAsset.address, - market.morphoBlue.chain.id, - ); - if (!collateralToken) return null; - - const isSelected = selectedMarkets.some((m) => m.uniqueKey === market.uniqueKey); - const collatToShow = market.collateralAsset.symbol - .slice(0, 6) - .concat(market.collateralAsset.symbol.length > 6 ? '...' : ''); - - return ( - { - if (isSelected) { - setSelectedMarkets( - selectedMarkets.filter((m) => m.uniqueKey !== market.uniqueKey), - ); - } else { - setSelectedMarkets([...selectedMarkets, market]); - } - }} - > - {/* Selection Indicator */} - - - {/* Collateral Asset */} - - - {/* Oracle */} - - - {/* LLTV */} - - - {/* Total Supply */} - - - {/* APY */} - - - {/* Risk Indicators */} - - - {/* Actions */} - - - ); - })} - -
- CollateralOracleLLTVTotal SupplyAPYRiskActions
-
- {isSelected && ( -
- - - -
- )} -
-
-
- -
-
- - {Number(market.lltv) / 1e16}% - - -

${formatReadable(Number(market.state.supplyAssetsUsd))}

-

- {formatReadable(formatBalance(market.state.supplyAssets, market.loanAsset.decimals))} {market.loanAsset.symbol} -

-
- - -
- - - -
-
- -
-
- )} -
- - {/* Navigation - ALWAYS VISIBLE */} -
- -
- {selectedMarkets.length > 0 && ( - - {selectedMarkets.length} market{selectedMarkets.length !== 1 ? 's' : ''} selected - - )} - -
-
-
- ); -} diff --git a/app/positions/components/onboarding/SetupPositions.tsx b/app/positions/components/onboarding/SetupPositions.tsx index 3f87d293..bc32cd0a 100644 --- a/app/positions/components/onboarding/SetupPositions.tsx +++ b/app/positions/components/onboarding/SetupPositions.tsx @@ -5,14 +5,12 @@ import Image from 'next/image'; import { formatUnits, parseUnits } from 'viem'; import { Button } from '@/components/common'; import Input from '@/components/Input/Input'; -import OracleVendorBadge from '@/components/OracleVendorBadge'; +import { MarketIdentity, MarketIdentityMode, MarketIdentityFocus } from '@/components/MarketIdentity'; import { SupplyProcessModal } from '@/components/SupplyProcessModal'; -import { TokenIcon } from '@/components/TokenIcon'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useMarketNetwork } from '@/hooks/useMarketNetwork'; import { useMultiMarketSupply } from '@/hooks/useMultiMarketSupply'; import { useStyledToast } from '@/hooks/useStyledToast'; -import { useUserBalancesAllNetworks } from '@/hooks/useUserBalances'; import { formatBalance } from '@/utils/balance'; import { SupportedNetworks } from '@/utils/networks'; import { APYCell } from 'app/markets/components/APYBreakdownTooltip'; @@ -20,8 +18,7 @@ import { useOnboarding } from './OnboardingContext'; export function SetupPositions() { const toast = useStyledToast(); - const { selectedToken, selectedMarkets, goToNextStep, goToPrevStep } = useOnboarding(); - const { balances } = useUserBalancesAllNetworks(); + const { selectedToken, selectedMarkets, goToNextStep, goToPrevStep, balances } = useOnboarding(); const [useEth] = useLocalStorage('useEth', false); const [usePermit2Setting] = useLocalStorage('usePermit2', true); const [totalAmount, setTotalAmount] = useState(''); @@ -293,9 +290,7 @@ export function SetupPositions() { - - - + @@ -304,43 +299,21 @@ export function SetupPositions() { {selectedMarkets.map((market) => { const currentPercentage = percentages[market.uniqueKey] ?? 0; const isLocked = lockedAmounts.has(market.uniqueKey); - const collatToShow = market.collateralAsset.symbol - .slice(0, 6) - .concat(market.collateralAsset.symbol.length > 6 ? '...' : ''); return ( - {/* Collateral Asset */} - - - {/* Oracle */} - - - {/* LLTV */} - {/* APY */} diff --git a/app/positions/components/onboarding/types.ts b/app/positions/components/onboarding/types.ts index 8ef31e45..35c365a5 100644 --- a/app/positions/components/onboarding/types.ts +++ b/app/positions/components/onboarding/types.ts @@ -8,14 +8,3 @@ export type TokenWithMarkets = NetworkToken & { 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/src/components/common/MarketSelectionModal.tsx b/src/components/common/MarketSelectionModal.tsx index e2b4a7cb..c6d8f6cb 100644 --- a/src/components/common/MarketSelectionModal.tsx +++ b/src/components/common/MarketSelectionModal.tsx @@ -144,7 +144,7 @@ export function MarketSelectionModal({ onKeyDown={handleBackdropKeyDown} aria-label="Close market selection" > -
+

{title}

@@ -161,15 +161,18 @@ export function MarketSelectionModal({

) : ( -
+
({ market: m, isSelected: selectedMarkets.has(m.uniqueKey), }))} + onToggleMarket={handleToggleMarket} disabled={false} uniqueCollateralTokens={undefined} + showSelectColumn={multiSelect} + itemsPerPage={7} />
)} diff --git a/src/components/common/MarketsTableWithSameLoanAsset.tsx b/src/components/common/MarketsTableWithSameLoanAsset.tsx index bd4bb915..bcb7e00a 100644 --- a/src/components/common/MarketsTableWithSameLoanAsset.tsx +++ b/src/components/common/MarketsTableWithSameLoanAsset.tsx @@ -1,4 +1,5 @@ import React, { useMemo, useState, useRef, useEffect } from 'react'; +import { Checkbox } from '@heroui/react'; import { ArrowDownIcon, ArrowUpIcon, ChevronDownIcon, TrashIcon } from '@radix-ui/react-icons'; import { motion, AnimatePresence } from 'framer-motion'; import Image from 'next/image'; @@ -27,6 +28,12 @@ type MarketsTableWithSameLoanAssetProps = { renderCartItemExtra?: (market: Market) => React.ReactNode; // Optional: Pass unique tokens for better filter performance uniqueCollateralTokens?: ERC20Token[]; + // Optional: Hide the select column (useful for single-select mode) + showSelectColumn?: boolean; + // Optional: Hide the cart/staging area showing selected markets + showCart?: boolean; + // Optional: entry per page + itemsPerPage?: number }; enum SortColumn { @@ -37,8 +44,6 @@ enum SortColumn { Risk = 4, } -const ITEMS_PER_PAGE = 8; - function HTSortable({ label, column, @@ -235,9 +240,11 @@ function CollateralFilter({ function OracleFilterComponent({ selectedOracles, setSelectedOracles, + availableOracles, }: { selectedOracles: PriceFeedVendors[]; setSelectedOracles: (oracles: PriceFeedVendors[]) => void; + availableOracles: PriceFeedVendors[]; }) { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); @@ -266,7 +273,7 @@ function OracleFilterComponent({ }; return ( -
+
    - {Object.values(PriceFeedVendors).map((oracle) => ( + {availableOracles.map((oracle) => (
  • void; disabled: boolean; + showSelectColumn: boolean; }) { const { market, isSelected } = marketWithSelection; @@ -370,22 +379,24 @@ function MarketRow({ } }} > -
- + )} + - - - - - @@ -426,6 +437,9 @@ export function MarketsTableWithSameLoanAsset({ disabled = false, renderCartItemExtra, uniqueCollateralTokens, + showSelectColumn = true, + showCart = true, + itemsPerPage = 8 }: MarketsTableWithSameLoanAssetProps): JSX.Element { const [currentPage, setCurrentPage] = useState(1); const [sortColumn, setSortColumn] = useState(SortColumn.Supply); @@ -444,7 +458,6 @@ export function MarketsTableWithSameLoanAsset({ // Get unique collaterals with full token data const availableCollaterals = useMemo(() => { - // If uniqueCollateralTokens is provided, use it (preferred approach from RiskSelection) if (uniqueCollateralTokens) { return uniqueCollateralTokens; } @@ -453,6 +466,11 @@ export function MarketsTableWithSameLoanAsset({ const tokenMap = new Map(); markets.forEach((m) => { + // Add null checks for nested properties + if (!m?.market?.collateralAsset?.address || !m?.market?.morphoBlue?.chain?.id) { + return; + } + const key = infoToKey(m.market.collateralAsset.address, m.market.morphoBlue.chain.id); if (!tokenMap.has(key)) { @@ -463,7 +481,7 @@ export function MarketsTableWithSameLoanAsset({ tokenMap.set(key, existingToken); } else { const token: UnknownERC20Token = { - symbol: m.market.collateralAsset.symbol, + symbol: m.market.collateralAsset.symbol ?? 'Unknown', img: undefined, decimals: m.market.collateralAsset.decimals ?? 18, networks: [{ @@ -479,6 +497,21 @@ export function MarketsTableWithSameLoanAsset({ return Array.from(tokenMap.values()).sort((a, b) => a.symbol.localeCompare(b.symbol)); }, [markets, uniqueCollateralTokens]); + // Get unique oracles from current markets + const availableOracles = useMemo(() => { + const oracleSet = new Set(); + + markets.forEach((m) => { + if (!m?.market?.morphoBlue?.chain?.id) return; + const vendorInfo = parsePriceFeedVendors(m.market.oracle?.data, m.market.morphoBlue.chain.id); + if (vendorInfo?.coreVendors) { + vendorInfo.coreVendors.forEach((vendor) => oracleSet.add(vendor)); + } + }); + + return Array.from(oracleSet); + }, [markets]); + // Filter and sort markets const processedMarkets = useMemo(() => { let filtered = [...markets]; @@ -486,6 +519,10 @@ export function MarketsTableWithSameLoanAsset({ // Apply collateral filter if (collateralFilter.length > 0) { filtered = filtered.filter((m) => { + // Add null checks + if (!m?.market?.collateralAsset?.address || !m?.market?.morphoBlue?.chain?.id) { + return false; + } const key = infoToKey(m.market.collateralAsset.address, m.market.morphoBlue.chain.id); return collateralFilter.some((filterKey) => filterKey.split('|').includes(key) @@ -496,8 +533,12 @@ export function MarketsTableWithSameLoanAsset({ // Apply oracle filter if (oracleFilter.length > 0) { filtered = filtered.filter((m) => { + // Add null checks + if (!m?.market?.morphoBlue?.chain?.id) { + return false; + } const vendorInfo = parsePriceFeedVendors(m.market.oracle?.data, m.market.morphoBlue.chain.id); - return vendorInfo.coreVendors.some((v) => oracleFilter.includes(v)); + return vendorInfo?.coreVendors?.some((v) => oracleFilter.includes(v)) ?? false; }); } @@ -506,20 +547,20 @@ export function MarketsTableWithSameLoanAsset({ let comparison = 0; switch (sortColumn) { case SortColumn.MarketName: - comparison = a.market.collateralAsset.symbol.localeCompare( - b.market.collateralAsset.symbol, + comparison = (a.market?.collateralAsset?.symbol ?? '').localeCompare( + b.market?.collateralAsset?.symbol ?? '', ); break; case SortColumn.Supply: comparison = - Number(a.market.state.supplyAssetsUsd) - Number(b.market.state.supplyAssetsUsd); + Number(a.market?.state?.supplyAssetsUsd ?? 0) - Number(b.market?.state?.supplyAssetsUsd ?? 0); break; case SortColumn.APY: - comparison = (a.market.state.supplyApy ?? 0) - (b.market.state.supplyApy ?? 0); + comparison = (a.market?.state?.supplyApy ?? 0) - (b.market?.state?.supplyApy ?? 0); break; case SortColumn.Liquidity: comparison = - Number(a.market.state.liquidityAssets) - Number(b.market.state.liquidityAssets); + Number(a.market?.state?.liquidityAssets ?? 0) - Number(b.market?.state?.liquidityAssets ?? 0); break; case SortColumn.Risk: comparison = 0; @@ -536,19 +577,28 @@ export function MarketsTableWithSameLoanAsset({ return markets.filter((m) => m.isSelected); }, [markets]); - // Pagination - const totalPages = Math.ceil(processedMarkets.length / ITEMS_PER_PAGE); - const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; - const paginatedMarkets = processedMarkets.slice(startIndex, startIndex + ITEMS_PER_PAGE); + // Pagination with guards to prevent invalid states + const safePerPage = Math.max(1, Math.floor(itemsPerPage ?? 8)); + const totalPages = Math.max(1, Math.ceil(processedMarkets.length / safePerPage)); + const safePage = Math.min(Math.max(1, currentPage), totalPages); + const startIndex = (safePage - 1) * safePerPage; + const paginatedMarkets = processedMarkets.slice(startIndex, startIndex + safePerPage); React.useEffect(() => { setCurrentPage(1); }, [collateralFilter, oracleFilter]); + // Clamp currentPage when totalPages changes + React.useEffect(() => { + if (currentPage > totalPages) { + setCurrentPage(totalPages); + } + }, [totalPages, currentPage]); + return (
{/* Cart/Staging Area - MarketDetailsBlock Style */} - {selectedMarkets.length > 0 && ( + {showCart && selectedMarkets.length > 0 && (
{selectedMarkets.map(({ market }) => (
@@ -602,7 +653,7 @@ export function MarketsTableWithSameLoanAsset({
CollateralOracleLLTVMarket APY Distribution
-
- - - {collatToShow} - -
-
-
- -
-
- - {Number(market.lltv) / 1e16}% - + {/* Market Identity */} + + -
- e.stopPropagation()} - /> -
-
+ {showSelectColumn && ( + +
+ e.stopPropagation()} + size='sm' + /> +
+
+ +

{formatReadable(formatBalance(market.state.supplyAssets, market.loanAsset.decimals))}

+

- {market.state.supplyApy ? `${(market.state.supplyApy * 100).toFixed(2)}%` : '—'} + {market.state.supplyApy ? `${(market.state.supplyApy * 100).toFixed(2)}` : '—'}

+ { market.state.supplyApy && % }
+

{formatReadable(formatBalance(market.state.liquidityAssets, market.loanAsset.decimals))}

+
- + {showSelectColumn && } {paginatedMarkets.length === 0 ? ( - @@ -649,6 +700,7 @@ export function MarketsTableWithSameLoanAsset({ marketWithSelection={marketWithSelection} onToggle={() => onToggleMarket(marketWithSelection.market.uniqueKey)} disabled={disabled} + showSelectColumn={showSelectColumn} /> )) )} @@ -659,10 +711,11 @@ export function MarketsTableWithSameLoanAsset({ {/* Pagination */} ); diff --git a/src/contexts/MarketsContext.tsx b/src/contexts/MarketsContext.tsx index bd135594..b16c1d92 100644 --- a/src/contexts/MarketsContext.tsx +++ b/src/contexts/MarketsContext.tsx @@ -129,7 +129,8 @@ export function MarketsProvider({ children }: MarketsProviderProps) { // Process combined markets (filters, warnings, liquidation status) // Existing filters seem appropriate const filtered = combinedMarkets - .filter((market) => market.collateralAsset != undefined) + .filter((market) => market.uniqueKey !== undefined) + .filter((market) => market.loanAsset && market.collateralAsset) // non of them is undefined or null .filter((market) => isSupportedChain(market.morphoBlue.chain.id)) // Keep this filter .filter((market) => !blacklistedMarkets.includes(market.uniqueKey)); diff --git a/src/utils/warnings.ts b/src/utils/warnings.ts index a617fb09..cd0ca0c5 100644 --- a/src/utils/warnings.ts +++ b/src/utils/warnings.ts @@ -157,14 +157,6 @@ export const getMarketWarningsWithDetail = ( ? monarchWhitelistedMarkets.find((m) => m.id === market.uniqueKey.toLowerCase()) : undefined; - if (market.uniqueKey.startsWith('0x34f676')) { - console.log('market', market); - } - - if (whitelistedMarketData) { - console.log('market', market); - } - // process official warnings for (const warning of market.warnings) { const foundWarning = allDetails.find((w) => w.code === warning.type); diff --git a/tailwind.config.ts b/tailwind.config.ts index 5d6a7c51..a744154a 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -71,6 +71,9 @@ const config: Config = { large: '0.375rem', // rounded }, }, + colors: { + content1: '#222529', // Modal background + }, }, }, }),
SelectSelectId
+ No markets found