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
111 changes: 56 additions & 55 deletions app/HomePage.tsx

Large diffs are not rendered by default.

20 changes: 17 additions & 3 deletions app/positions/components/PositionsContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import EmptyScreen from '@/components/Status/EmptyScreen';
import LoadingScreen from '@/components/Status/LoadingScreen';
import { SupplyModalV2 } from '@/components/SupplyModalV2';
import { useMarkets } from '@/hooks/useMarkets';
import useUserPositionsSummaryData from '@/hooks/useUserPositionsSummaryData';
import useUserPositionsSummaryData, { EarningsPeriod } from '@/hooks/useUserPositionsSummaryData';
import { MarketPosition } from '@/utils/types';
import { OnboardingModal } from './onboarding/OnboardingModal';
import { PositionsSummaryTable } from './PositionsSummaryTable';
Expand All @@ -26,6 +26,7 @@ export default function Positions() {
const [showWithdrawModal, setShowWithdrawModal] = useState<boolean>(false);
const [showOnboardingModal, setShowOnboardingModal] = useState<boolean>(false);
const [selectedPosition, setSelectedPosition] = useState<MarketPosition | null>(null);
const [earningsPeriod, setEarningsPeriod] = useState<EarningsPeriod>('day');

const { account } = useParams<{ account: string }>();
const { address } = useAccount();
Expand All @@ -49,10 +50,21 @@ export default function Positions() {
isRefetching,
positions: marketPositions,
refetch,
} = useUserPositionsSummaryData(account);
loadingStates,
} = useUserPositionsSummaryData(account, earningsPeriod);

const loading = isMarketsLoading || isPositionsLoading;

// Generate loading message based on current state
const loadingMessage = useMemo(() => {
if (isMarketsLoading) return 'Loading markets...';
if (loadingStates.positions) return 'Loading user positions...';
if (loadingStates.blocks) return 'Fetching block numbers...';
if (loadingStates.snapshots) return 'Loading historical snapshots...';
if (loadingStates.transactions) return 'Loading transaction history...';
return 'Loading...';
}, [isMarketsLoading, loadingStates]);

const hasSuppliedMarkets = marketPositions && marketPositions.length > 0;

const handleRefetch = () => {
Expand Down Expand Up @@ -122,7 +134,7 @@ export default function Positions() {
/>

{loading ? (
<LoadingScreen message="Loading Supplies..." className="mt-10" />
<LoadingScreen message={loadingMessage} className="mt-10" />
) : !hasSuppliedMarkets ? (
<div className="container flex flex-col">
<div className="flex w-full justify-end">
Expand Down Expand Up @@ -151,6 +163,8 @@ export default function Positions() {
refetch={() => void refetch()}
isRefetching={isRefetching}
isLoadingEarnings={isEarningsLoading}
earningsPeriod={earningsPeriod}
setEarningsPeriod={setEarningsPeriod}
/>
</div>
)}
Expand Down
20 changes: 11 additions & 9 deletions app/positions/components/PositionsSummaryTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ import { TooltipContent } from '@/components/TooltipContent';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import { computeMarketWarnings } from '@/hooks/useMarketWarnings';
import { useStyledToast } from '@/hooks/useStyledToast';
import { EarningsPeriod } from '@/hooks/useUserPositionsSummaryData';
import { formatReadable, formatBalance } from '@/utils/balance';
import { getNetworkImg } from '@/utils/networks';
import {
EarningsPeriod,
getGroupedEarnings,
groupPositionsByLoanAsset,
processCollaterals,
Expand Down Expand Up @@ -127,6 +127,8 @@ type PositionsSummaryTableProps = {
refetch: (onSuccess?: () => void) => void;
isRefetching: boolean;
isLoadingEarnings?: boolean;
earningsPeriod: EarningsPeriod;
setEarningsPeriod: (period: EarningsPeriod) => void;
};

export function PositionsSummaryTable({
Expand All @@ -138,14 +140,14 @@ export function PositionsSummaryTable({
isRefetching,
isLoadingEarnings,
account,
earningsPeriod,
setEarningsPeriod,
}: PositionsSummaryTableProps) {
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
const [showRebalanceModal, setShowRebalanceModal] = useState(false);
const [selectedGroupedPosition, setSelectedGroupedPosition] = useState<GroupedPosition | null>(
null,
);

const [earningsPeriod, setEarningsPeriod] = useState<EarningsPeriod>(EarningsPeriod.Day);
const [showEmptyPositions, setShowEmptyPositions] = useLocalStorage<boolean>(
PositionsShowEmptyKey,
false,
Expand All @@ -164,10 +166,10 @@ export function PositionsSummaryTable({
}, [account, address]);

const periodLabels: Record<EarningsPeriod, string> = {
[EarningsPeriod.All]: 'All Time',
[EarningsPeriod.Day]: '1D',
[EarningsPeriod.Week]: '7D',
[EarningsPeriod.Month]: '30D',
'all': 'All Time',
'day': '1D',
'week': '7D',
'month': '30D',
};

const groupedPositions = useMemo(
Expand Down Expand Up @@ -329,7 +331,7 @@ export function PositionsSummaryTable({
const isExpanded = expandedRows.has(rowKey);
const avgApy = groupedPosition.totalWeightedApy;

const earnings = getGroupedEarnings(groupedPosition, earningsPeriod);
const earnings = getGroupedEarnings(groupedPosition);

return (
<React.Fragment key={rowKey}>
Expand Down Expand Up @@ -376,7 +378,7 @@ export function PositionsSummaryTable({
) : (
<span className="font-medium">
{(() => {
if (earnings === null) return '-';
if (earnings === 0n) return '-';
return (
formatReadable(
Number(
Expand Down
85 changes: 83 additions & 2 deletions src/components/Status/LoadingScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
'use client';

import { useState, useEffect } from 'react';
import Image from 'next/image';
import { BarLoader } from 'react-spinners';
import loadingImg from '../imgs/aragon/loading.png';
Expand All @@ -7,7 +10,83 @@ type LoadingScreenProps = {
className?: string;
};

export default function LoadingScreen({ message = 'Loading...', className }: LoadingScreenProps) {
const loadingPhrases = [
'Loading...',
'Fetching data...',
'Almost there...',
'Preparing your view...',
'Connecting to Morpho...',
];

function TypingAnimation({ phrases }: { phrases: string[] }) {
const [displayText, setDisplayText] = useState('');
const [phraseIndex, setPhraseIndex] = useState(0);
const [isDeleting, setIsDeleting] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const [showCursor, setShowCursor] = useState(true);

useEffect(() => {
const cursorInterval = setInterval(() => {
setShowCursor((prev) => !prev);
}, 530);

return () => clearInterval(cursorInterval);
}, []);

useEffect(() => {
if (isPaused) {
const pauseTimeout = setTimeout(() => {
setIsPaused(false);
setIsDeleting(true);
}, 1500);
return () => clearTimeout(pauseTimeout);
}

const currentPhrase = phrases[phraseIndex];
const targetText = currentPhrase;

const getNextPhraseIndex = (current: number) => (current + 1) % phrases.length;

const typingSpeed = 40;
const deletingSpeed = 25;

const timeout = setTimeout(() => {
if (!isDeleting) {
if (displayText.length < targetText.length) {
setDisplayText(targetText.slice(0, displayText.length + 1));
} else {
setIsPaused(true);
}
} else {
if (displayText.length > 0) {
setDisplayText(displayText.slice(0, -1));
} else {
setIsDeleting(false);
setPhraseIndex(getNextPhraseIndex(phraseIndex));
}
}
}, isDeleting ? deletingSpeed : typingSpeed);

return () => clearTimeout(timeout);
}, [displayText, phraseIndex, isDeleting, isPaused, phrases]);

return (
<span className="inline-flex items-center">
<span>{displayText}</span>
<span
className="ml-0.5 inline-block"
style={{ opacity: showCursor ? 1 : 0, transition: 'opacity 0.1s' }}
>
|
</span>
</span>
);
}

export default function LoadingScreen({ message, className }: LoadingScreenProps) {
const phrases = message ? [message] : loadingPhrases;
const showTyping = !message;

return (
<div
className={`bg-surface my-4 flex min-h-48 flex-col items-center justify-center space-y-4 rounded py-8 shadow-sm font-zen ${
Expand All @@ -16,7 +95,9 @@ export default function LoadingScreen({ message = 'Loading...', className }: Loa
>
<Image src={loadingImg} alt="Logo" width={200} height={200} className="py-4" />
<BarLoader width={100} color="#f45f2d" height={2} className="pb-1" />
<p className="pt-4 text-center text-secondary">{message}</p>
<p className="pt-4 text-center text-secondary">
{showTyping ? <TypingAnimation phrases={phrases} /> : message}
</p>
</div>
);
}
4 changes: 2 additions & 2 deletions src/components/layout/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ function Header({ ghost }: HeaderProps) {

return (
<>
<div className="h-[120px] w-full" /> {/* Spacer div */}
<div className="h-[80px] w-full md:h-[120px]" /> {/* Spacer div */}
<header
data-scroll-state={scrollState}
className="bg-main fixed left-0 right-0 top-0 flex h-[120px] justify-center pt-8"
className="bg-main fixed left-0 right-0 top-0 flex h-[80px] justify-center pt-4 md:h-[120px] md:pt-8"
style={{ zIndex: 40 }} // Lower z-index to work with modal backdrop
>
<Menu />
Expand Down
125 changes: 67 additions & 58 deletions src/hooks/usePositionReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
filterTransactionsInPeriod,
} from '@/utils/interest';
import { SupportedNetworks } from '@/utils/networks';
import { fetchPositionSnapshot } from '@/utils/positions';
import { fetchPositionsSnapshots } from '@/utils/positions';
import { estimatedBlockNumber, getClient } from '@/utils/rpc';
import { Market, MarketPosition, UserTransaction } from '@/utils/types';
import { useCustomRpc } from './useCustomRpc';
Expand Down Expand Up @@ -104,63 +104,72 @@ export const usePositionReport = (
}
}

const marketReports = (
await Promise.all(
relevantPositions.map(async (position) => {
const publicClient = getClient(
position.market.morphoBlue.chain.id,
customRpcUrls[position.market.morphoBlue.chain.id as SupportedNetworks] ?? undefined,
);
const startSnapshot = await fetchPositionSnapshot(
position.market.uniqueKey,
account,
position.market.morphoBlue.chain.id,
startBlockNumber,
publicClient,
);
const endSnapshot = await fetchPositionSnapshot(
position.market.uniqueKey,
account,
position.market.morphoBlue.chain.id,
endBlockNumber,
publicClient,
);

if (!startSnapshot || !endSnapshot) {
return;
}

const marketTransactions = filterTransactionsInPeriod(
allTransactions.filter(
(tx) => tx.data?.market?.uniqueKey === position.market.uniqueKey,
),
startTimestamp,
endTimestamp,
);

const earnings = calculateEarningsFromSnapshot(
BigInt(endSnapshot.supplyAssets),
BigInt(startSnapshot.supplyAssets),
marketTransactions,
startTimestamp,
endTimestamp,
);

return {
market: position.market,
interestEarned: earnings.earned,
totalDeposits: earnings.totalDeposits,
totalWithdraws: earnings.totalWithdraws,
apy: earnings.apy,
avgCapital: earnings.avgCapital,
effectiveTime: earnings.effectiveTime,
startBalance: BigInt(startSnapshot.supplyAssets),
endBalance: BigInt(endSnapshot.supplyAssets),
transactions: marketTransactions,
};
}),
)
).filter((report) => report !== null && report !== undefined) as PositionReport[];
// Batch fetch all snapshots using multicall
const marketIds = relevantPositions.map((position) => position.market.uniqueKey);
const publicClient = getClient(
selectedAsset.chainId as SupportedNetworks,
customRpcUrls[selectedAsset.chainId as SupportedNetworks] ?? undefined,
);

// Fetch start and end snapshots in parallel (batched per block number)
const [startSnapshots, endSnapshots] = await Promise.all([
fetchPositionsSnapshots(
marketIds,
account,
selectedAsset.chainId,
startBlockNumber,
publicClient,
),
fetchPositionsSnapshots(
marketIds,
account,
selectedAsset.chainId,
endBlockNumber,
publicClient,
),
]);

// Process positions with their snapshots
const marketReports = relevantPositions
.map((position) => {
const marketKey = position.market.uniqueKey;
const startSnapshot = startSnapshots.get(marketKey);
const endSnapshot = endSnapshots.get(marketKey);

if (!startSnapshot || !endSnapshot) {
return null;
}

const marketTransactions = filterTransactionsInPeriod(
allTransactions.filter(
(tx) => tx.data?.market?.uniqueKey === marketKey,
),
startTimestamp,
endTimestamp,
);

const earnings = calculateEarningsFromSnapshot(
BigInt(endSnapshot.supplyAssets),
BigInt(startSnapshot.supplyAssets),
marketTransactions,
startTimestamp,
endTimestamp,
);

return {
market: position.market,
interestEarned: earnings.earned,
totalDeposits: earnings.totalDeposits,
totalWithdraws: earnings.totalWithdraws,
apy: earnings.apy,
avgCapital: earnings.avgCapital,
effectiveTime: earnings.effectiveTime,
startBalance: BigInt(startSnapshot.supplyAssets),
endBalance: BigInt(endSnapshot.supplyAssets),
transactions: marketTransactions,
};
})
.filter((report): report is PositionReport => report !== null);

const totalInterestEarned = marketReports.reduce(
(sum, report) => sum + BigInt(report.interestEarned),
Expand Down
Loading