diff --git a/app/admin/stats/page.tsx b/app/admin/stats/page.tsx
index 8ac7228b..f9c1b9c0 100644
--- a/app/admin/stats/page.tsx
+++ b/app/admin/stats/page.tsx
@@ -13,12 +13,15 @@ import { PlatformStats, TimeFrame, AssetVolumeData } from '@/utils/statsUtils';
import { AssetMetricsTable } from './components/AssetMetricsTable';
import { StatsOverviewCards } from './components/StatsOverviewCards';
-// API endpoints mapping for different networks
-const API_ENDPOINTS = {
- [SupportedNetworks.Base]:
- 'https://api.studio.thegraph.com/query/94369/monarch-metrics/version/latest',
- [SupportedNetworks.Mainnet]:
- 'https://api.studio.thegraph.com/query/94369/monarch-metrics-mainnet/version/latest',
+const getAPIEndpoint = (network: SupportedNetworks) => {
+ switch (network) {
+ case SupportedNetworks.Base:
+ return 'https://api.studio.thegraph.com/query/94369/monarch-metrics/version/latest';
+ case SupportedNetworks.Mainnet:
+ return 'https://api.studio.thegraph.com/query/94369/monarch-metrics-mainnet/version/latest';
+ default:
+ return undefined;
+ }
};
export default function StatsPage() {
@@ -55,10 +58,13 @@ export default function StatsPage() {
const startTime = performance.now();
// Get API endpoint for the selected network
- const apiEndpoint = API_ENDPOINTS[selectedNetwork];
+ const apiEndpoint = getAPIEndpoint(selectedNetwork);
+ if (!apiEndpoint) {
+ throw new Error(`Unsupported network: ${selectedNetwork}`);
+ }
console.log(`Using API endpoint: ${apiEndpoint}`);
- const allStats = await fetchAllStatistics(timeframe, selectedNetwork, apiEndpoint);
+ const allStats = await fetchAllStatistics(selectedNetwork, apiEndpoint, timeframe);
const endTime = performance.now();
console.log(`Statistics fetched in ${endTime - startTime}ms:`, allStats);
diff --git a/app/api/balances/route.ts b/app/api/balances/route.ts
index f4376d62..4c7bb251 100644
--- a/app/api/balances/route.ts
+++ b/app/api/balances/route.ts
@@ -4,6 +4,7 @@ const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY;
const ALCHEMY_URLS = {
'1': `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`,
'8453': `https://base-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`,
+ '137': `https://polygon-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`,
};
type TokenBalance = {
diff --git a/app/api/block/route.ts b/app/api/block/route.ts
index a938b5d5..0297f3b1 100644
--- a/app/api/block/route.ts
+++ b/app/api/block/route.ts
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { PublicClient } from 'viem';
import { SmartBlockFinder } from '@/utils/blockFinder';
import { SupportedNetworks } from '@/utils/networks';
-import { mainnetClient, baseClient } from '@/utils/rpc';
+import { getClient } from '@/utils/rpc';
const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY;
@@ -42,7 +42,7 @@ export async function GET(request: NextRequest) {
const numericTimestamp = parseInt(timestamp);
// Fallback to SmartBlockFinder
- const client = numericChainId === SupportedNetworks.Mainnet ? mainnetClient : baseClient;
+ const client = getClient(numericChainId as SupportedNetworks);
// Try Etherscan API first
const etherscanBlock = await getBlockFromEtherscan(numericTimestamp, numericChainId);
diff --git a/app/api/positions/historical/route.ts b/app/api/positions/historical/route.ts
index c897ce73..40448ee8 100644
--- a/app/api/positions/historical/route.ts
+++ b/app/api/positions/historical/route.ts
@@ -1,9 +1,9 @@
import { NextRequest, NextResponse } from 'next/server';
import { Address } from 'viem';
import morphoABI from '@/abis/morpho';
-import { MORPHO } from '@/utils/morpho';
+import { getMorphoAddress } from '@/utils/morpho';
import { SupportedNetworks } from '@/utils/networks';
-import { baseClient, mainnetClient } from '@/utils/rpc';
+import { getClient } from '@/utils/rpc';
// Types
type Position = {
@@ -60,13 +60,13 @@ async function getPositionAtBlock(
console.log(`Get user position ${marketId.slice(0, 6)} at current block`);
}
- const client = chainId === SupportedNetworks.Mainnet ? mainnetClient : baseClient;
+ const client = getClient(chainId as SupportedNetworks);
if (!client) throw new Error(`Unsupported chain ID: ${chainId}`);
try {
// First get the position data
const positionArray = (await client.readContract({
- address: MORPHO,
+ address: getMorphoAddress(chainId as SupportedNetworks),
abi: morphoABI,
functionName: 'position',
args: [marketId as `0x${string}`, userAddress as Address],
@@ -94,7 +94,7 @@ async function getPositionAtBlock(
// Only fetch market data if position has shares
const marketArray = (await client.readContract({
- address: MORPHO,
+ address: getMorphoAddress(chainId as SupportedNetworks),
abi: morphoABI,
functionName: 'market',
args: [marketId as `0x${string}`],
diff --git a/app/history/components/HistoryContent.tsx b/app/history/components/HistoryContent.tsx
index da2c499f..df5b6bdd 100644
--- a/app/history/components/HistoryContent.tsx
+++ b/app/history/components/HistoryContent.tsx
@@ -8,7 +8,7 @@ import { HistoryTable } from './HistoryTable';
export default function HistoryContent({ account }: { account: string }) {
const { data: positions } = useUserPositions(account, true);
- const { rebalancerInfo } = useUserRebalancerInfo(account);
+ const { rebalancerInfos } = useUserRebalancerInfo(account);
return (
@@ -17,7 +17,7 @@ export default function HistoryContent({ account }: { account: string }) {
Transaction History
-
+
diff --git a/app/history/components/HistoryTable.tsx b/app/history/components/HistoryTable.tsx
index ea298faa..2098ca95 100644
--- a/app/history/components/HistoryTable.tsx
+++ b/app/history/components/HistoryTable.tsx
@@ -27,7 +27,7 @@ import {
type HistoryTableProps = {
account: string | undefined;
positions: MarketPosition[];
- rebalancerInfo?: UserRebalancerInfo;
+ rebalancerInfos: UserRebalancerInfo[];
};
type AssetKey = {
@@ -37,7 +37,7 @@ type AssetKey = {
decimals: number;
};
-export function HistoryTable({ account, positions, rebalancerInfo }: HistoryTableProps) {
+export function HistoryTable({ account, positions, rebalancerInfos }: HistoryTableProps) {
const [selectedAsset, setSelectedAsset] = useState(null);
const [isOpen, setIsOpen] = useState(false);
const [query, setQuery] = useState('');
@@ -307,7 +307,12 @@ export function HistoryTable({ account, positions, rebalancerInfo }: HistoryTabl
const sign = tx.type === UserTxTypes.MarketSupply ? '+' : '-';
const lltv = Number(formatUnits(BigInt(market.lltv), 18)) * 100;
- const isAgent = rebalancerInfo?.transactions.some(
+ // Find the rebalancer info for the specific network of the transaction
+ const networkRebalancerInfo = rebalancerInfos.find(
+ (info) => info.network === market.morphoBlue.chain.id,
+ );
+ // Check if the transaction hash exists in the transactions of the found rebalancer info
+ const isAgent = networkRebalancerInfo?.transactions.some(
(agentTx) => agentTx.transactionHash === tx.hash,
);
diff --git a/app/positions/components/PositionsContent.tsx b/app/positions/components/PositionsContent.tsx
index 969330ac..39616f1e 100644
--- a/app/positions/components/PositionsContent.tsx
+++ b/app/positions/components/PositionsContent.tsx
@@ -19,7 +19,7 @@ import LoadingScreen from '@/components/Status/LoadingScreen';
import { SupplyModalV2 } from '@/components/SupplyModalV2';
import useUserPositionsSummaryData from '@/hooks/useUserPositionsSummaryData';
import { useUserRebalancerInfo } from '@/hooks/useUserRebalancerInfo';
-import { SupportedNetworks } from '@/utils/networks';
+import { isAgentAvailable } from '@/utils/networks';
import { MarketPosition } from '@/utils/types';
import { SetupAgentModal } from './agent/SetupAgentModal';
import { OnboardingModal } from './onboarding/Modal';
@@ -34,7 +34,7 @@ export default function Positions() {
const { account } = useParams<{ account: string }>();
const { address } = useAccount();
- const { rebalancerInfo, refetch: refetchRebalancerInfo } = useUserRebalancerInfo(account);
+ const { rebalancerInfos, refetch: refetchRebalancerInfo } = useUserRebalancerInfo(account);
const isOwner = useMemo(() => {
if (!account) return false;
@@ -51,9 +51,9 @@ export default function Positions() {
const hasSuppliedMarkets = marketPositions && marketPositions.length > 0;
- const hasActivePositionOnBase = marketPositions?.some((position) => {
+ const hasActivePositionForAgent = marketPositions?.some((position) => {
return (
- position.market.morphoBlue.chain.id === SupportedNetworks.Base &&
+ isAgentAvailable(position.market.morphoBlue.chain.id) &&
BigInt(position.state.supplyShares) > 0
);
});
@@ -84,7 +84,7 @@ export default function Positions() {
Report
- {isOwner && hasActivePositionOnBase && (
+ {isOwner && hasActivePositionForAgent && (
+
+
+
+
+
+
+
+
+
+ Show Empty Positions
+
+
+
+
+
+ Show Collateral Exposure
+
+
+
+
+
@@ -191,7 +257,7 @@ export function PositionsSummaryTable({
{processedPositions.map((groupedPosition) => {
const rowKey = `${groupedPosition.loanAssetAddress}-${groupedPosition.chainId}`;
const isExpanded = expandedRows.has(rowKey);
- const avgApy = groupedPosition.totalWeightedApy / groupedPosition.totalSupply;
+ const avgApy = groupedPosition.totalWeightedApy;
const earnings = getGroupedEarnings(groupedPosition, earningsPeriod);
@@ -335,6 +401,8 @@ export function PositionsSummaryTable({
setShowWithdrawModal={setShowWithdrawModal}
setShowSupplyModal={setShowSupplyModal}
setSelectedPosition={setSelectedPosition}
+ showEmptyPositions={showEmptyPositions}
+ showCollateralExposure={showCollateralExposure}
/>
diff --git a/app/positions/components/SuppliedMarketsDetail.tsx b/app/positions/components/SuppliedMarketsDetail.tsx
index 9804e5e8..d1512fe8 100644
--- a/app/positions/components/SuppliedMarketsDetail.tsx
+++ b/app/positions/components/SuppliedMarketsDetail.tsx
@@ -14,6 +14,8 @@ type SuppliedMarketsDetailProps = {
setShowWithdrawModal: (show: boolean) => void;
setShowSupplyModal: (show: boolean) => void;
setSelectedPosition: (position: MarketPosition) => void;
+ showEmptyPositions: boolean;
+ showCollateralExposure: boolean;
};
function WarningTooltip({ warnings }: { warnings: WarningWithDetail[] }) {
@@ -42,14 +44,25 @@ export function SuppliedMarketsDetail({
setShowWithdrawModal,
setShowSupplyModal,
setSelectedPosition,
+ showEmptyPositions,
+ showCollateralExposure,
}: SuppliedMarketsDetailProps) {
- // Sort active markets by size
- const sortedActiveMarkets = [...groupedPosition.markets].sort(
+ // Sort active markets by size first
+ const sortedMarkets = [...groupedPosition.markets].sort(
(a, b) =>
Number(formatBalance(b.state.supplyAssets, b.market.loanAsset.decimals)) -
Number(formatBalance(a.state.supplyAssets, a.market.loanAsset.decimals)),
);
+ // Filter based on the showEmptyPositions prop
+ const filteredMarkets = showEmptyPositions
+ ? sortedMarkets
+ : sortedMarkets.filter(
+ (position) =>
+ Number(formatBalance(position.state.supplyAssets, position.market.loanAsset.decimals)) >
+ 0,
+ );
+
const totalSupply = groupedPosition.totalSupply;
const getWarningColor = (warnings: WarningWithDetail[]) => {
@@ -67,44 +80,47 @@ export function SuppliedMarketsDetail({
className="overflow-hidden"
>
-
-
-
Collateral Exposure
-
- {groupedPosition.processedCollaterals.map((collateral, colIndex) => (
-
- ))}
-
-
- {groupedPosition.processedCollaterals.map((collateral, colIndex) => (
-
-
+
+
Collateral Exposure
+
+ {groupedPosition.processedCollaterals.map((collateral, colIndex) => (
+
- ■
- {' '}
- {collateral.symbol}: {formatReadable(collateral.percentage)}%
-
- ))}
+ title={`${collateral.symbol}: ${collateral.percentage.toFixed(2)}%`}
+ />
+ ))}
+
+
+ {groupedPosition.processedCollaterals.map((collateral, colIndex) => (
+
+
+ ■
+ {' '}
+ {collateral.symbol}: {formatReadable(collateral.percentage)}%
+
+ ))}
+
-
+ )}
{/* Markets Table - Always visible */}
@@ -121,7 +137,7 @@ export function SuppliedMarketsDetail({
- {sortedActiveMarkets.map((position) => {
+ {filteredMarkets.map((position) => {
const suppliedAmount = Number(
formatBalance(position.state.supplyAssets, position.market.loanAsset.decimals),
);
diff --git a/app/positions/components/agent/Main.tsx b/app/positions/components/agent/Main.tsx
index 947175dc..4bb1b91a 100644
--- a/app/positions/components/agent/Main.tsx
+++ b/app/positions/components/agent/Main.tsx
@@ -7,55 +7,41 @@ import { Button } from '@/components/common';
import { TokenIcon } from '@/components/TokenIcon';
import { TooltipContent } from '@/components/TooltipContent';
import { useMarkets } from '@/contexts/MarketsContext';
+import { getExplorerURL } from '@/utils/external';
import { findAgent } from '@/utils/monarch-agent';
+import { getNetworkName } from '@/utils/networks';
import { UserRebalancerInfo } from '@/utils/types';
const img = require('../../../../src/imgs/agent/agent-detailed.png') as string;
type MainProps = {
- account?: string;
onNext: () => void;
- userRebalancerInfo: UserRebalancerInfo;
+ userRebalancerInfos: UserRebalancerInfo[];
};
-export function Main({ account, onNext, userRebalancerInfo }: MainProps) {
- const agent = findAgent(userRebalancerInfo.rebalancer);
+export function Main({ onNext, userRebalancerInfos }: MainProps) {
const { markets } = useMarkets();
- if (!agent) {
- return null;
- }
-
- // Find markets that the agent is authorized to manage
- const authorizedMarkets = markets.filter((market) =>
- userRebalancerInfo.marketCaps.some(
- (cap) => cap.marketId.toLowerCase() === market.uniqueKey.toLowerCase(),
- ),
- );
+ const activeAgentInfos = userRebalancerInfos
+ .map((info) => ({
+ info,
+ agent: findAgent(info.rebalancer),
+ }))
+ .filter((item) => item.agent !== undefined);
- // Group markets by loan asset address
- const loanAssetGroups = authorizedMarkets.reduce(
- (acc, market) => {
- const address = market.loanAsset.address.toLowerCase();
- if (!acc[address]) {
- acc[address] = {
- address,
- chainId: market.morphoBlue.chain.id,
- markets: [],
- symbol: market.loanAsset.symbol,
- };
- }
- acc[address].markets.push(market);
- return acc;
- },
- {} as Record<
- string,
- { address: string; chainId: number; symbol: string; markets: typeof authorizedMarkets }
- >,
- );
+ if (activeAgentInfos.length === 0) {
+ return (
+
+
No active agent found for the configured networks.
+
+
+ );
+ }
return (
-
+
-
-
-
-
{agent.name}
-
- {agent.address.slice(0, 6) + '...' + agent.address.slice(-4)}
-
-
-
}
- title="Agent Active"
- detail="Your agent is actively managing your positions"
- />
+ {activeAgentInfos.map(({ info, agent }) => {
+ if (!agent) return null;
+
+ const networkName = getNetworkName(info.network);
+
+ const authorizedMarkets = markets.filter(
+ (market) =>
+ market.morphoBlue.chain.id === info.network &&
+ info.marketCaps.some(
+ (cap) => cap.marketId.toLowerCase() === market.uniqueKey.toLowerCase(),
+ ),
+ );
+
+ const loanAssetGroups = authorizedMarkets.reduce(
+ (acc, market) => {
+ const address = market.loanAsset.address.toLowerCase();
+ if (!acc[address]) {
+ acc[address] = {
+ address,
+ chainId: market.morphoBlue.chain.id,
+ markets: [],
+ symbol: market.loanAsset.symbol,
+ };
}
- >
-
-
-
+ acc[address].markets.push(market);
+ return acc;
+ },
+ {} as Record<
+ string,
+ { address: string; chainId: number; symbol: string; markets: typeof authorizedMarkets }
+ >,
+ );
-
-
-
Strategy
-
{agent.strategyDescription}
-
+ const explorerUrl = getExplorerURL(agent.address, info.network);
-
-
Monitoring Positions
-
- {Object.values(loanAssetGroups).map(
- ({ address, chainId, markets: marketsForLoanAsset, symbol }) => {
- return (
-
-
-
- {symbol ?? 'Unknown'} ({marketsForLoanAsset.length})
-
-
- );
- },
- )}
+ return (
+
+
+
+
{agent.name}
+
+ {networkName}
+
+
+ {agent.address.slice(0, 6) + '...' + agent.address.slice(-4)}
+
+
+
}
+ title="Agent Active"
+ detail={`Agent is active on ${networkName}`}
+ />
+ }
+ >
+
+
-
-
-
Automations
-
-
- View All ({userRebalancerInfo.transactions?.length ?? 0})
-
+
+
+
Strategy
+
{agent.strategyDescription}
+
+
+
+
Monitoring Positions
+
+ {Object.values(loanAssetGroups).map(
+ ({ address, chainId, markets: marketsForLoanAsset, symbol }) => {
+ return (
+
+
+
+ {symbol ?? 'Unknown'} ({marketsForLoanAsset.length})
+
+
+ );
+ },
+ )}
+ {Object.values(loanAssetGroups).length === 0 && (
+
+ No markets currently configured for this agent.
+
+ )}
+
+
-
-
+ );
+ })}