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, + }; } - > -
-
- Active -
- -
+ 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}`} + /> + } + > +
+
+ Active +
+
-
-
-

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. +

+ )} +
+
-
-
+ ); + })} + + + {availableNetworks.map((networkId) => ( + +
+ + {getNetworkName(networkId) ?? `Network ${networkId}`} +
+
+ ))} +
+ + )}
+ {groupedMarkets.length === 0 && ( +

+ No active supplied markets found for {getNetworkName(targetNetwork)}. +

+ )} {groupedMarkets.map((group) => { - const groupKey = group.loanAsset.address; + const groupKey = `${group.loanAsset.address}-${group.network}`; const isExpanded = expandedGroups.includes(groupKey); const numMarketsToAdd = [ @@ -228,6 +348,7 @@ export function SetupAgent({ onClose={() => setShowProcessModal(false)} /> )} + @@ -271,7 +400,6 @@ export function SetupAgent({ className="overflow-hidden" >
- {/* Authorized markets Markets */} {group.authorizedMarkets.length > 0 && (

Authorized

@@ -285,12 +413,12 @@ export function SetupAgent({ ? removeFromPendingCaps(market) : addToPendingCaps(market, BigInt(0)) } + isDisabled={false} /> ))}
)} - {/* Active Markets */} {group.activeMarkets.length > 0 && (

Active Markets

@@ -304,12 +432,12 @@ export function SetupAgent({ ? addToPendingCaps(market, maxUint256) : removeFromPendingCaps(market) } + isDisabled={false} /> ))}
)} - {/* Historical Markets */} {group.historicalMarkets.length > 0 && (

Previously Used

@@ -323,12 +451,12 @@ export function SetupAgent({ ? addToPendingCaps(market, maxUint256) : removeFromPendingCaps(market) } + isDisabled={false} /> ))}
)} - {/* Other Markets */} {group.otherMarkets.length > 0 && !showAllMarkets && (
@@ -366,7 +495,6 @@ export function SetupAgent({ })}
- {/* Footer */}
diff --git a/app/positions/components/agent/SetupAgentModal.tsx b/app/positions/components/agent/SetupAgentModal.tsx index 049e5445..e3e1abd5 100644 --- a/app/positions/components/agent/SetupAgentModal.tsx +++ b/app/positions/components/agent/SetupAgentModal.tsx @@ -6,7 +6,7 @@ import { useMarkets } from '@/contexts/MarketsContext'; import { MarketCap } from '@/hooks/useAuthorizeAgent'; import useUserPositions from '@/hooks/useUserPositions'; import { findAgent } from '@/utils/monarch-agent'; -import { SupportedNetworks } from '@/utils/networks'; +import { isAgentAvailable } from '@/utils/networks'; import { Market, UserRebalancerInfo } from '@/utils/types'; import { Main as MainContent } from './Main'; import { SetupAgent } from './SetupAgent'; @@ -66,14 +66,14 @@ type SetupAgentModalProps = { account?: Address; isOpen: boolean; onClose: () => void; - userRebalancerInfo?: UserRebalancerInfo; + userRebalancerInfos: UserRebalancerInfo[]; }; export function SetupAgentModal({ account, isOpen, onClose, - userRebalancerInfo, + userRebalancerInfos, }: SetupAgentModalProps) { const [currentStep, setCurrentStep] = useState(SetupStep.Main); const [pendingCaps, setPendingCaps] = useState([]); @@ -122,12 +122,13 @@ export function SetupAgentModal({ ]); }; - const removeFromCaps = (market: Market) => { + const removeFromPendingCaps = (market: Market) => { setPendingCaps((prev) => prev.filter((cap) => cap.market.uniqueKey !== market.uniqueKey)); }; - const hasSetupAgent = - !!userRebalancerInfo && findAgent(userRebalancerInfo.rebalancer) !== undefined; + const hasSetupAgent = userRebalancerInfos.some( + (info) => findAgent(info.rebalancer) !== undefined, + ); return ( )} {currentStep === SetupStep.Setup && ( m.morphoBlue.chain.id === SupportedNetworks.Base, - )} + allMarkets={allMarkets.filter((m) => isAgentAvailable(m.morphoBlue.chain.id))} + userRebalancerInfos={userRebalancerInfos} pendingCaps={pendingCaps} addToPendingCaps={addToPendingCaps} - removeFromPendingCaps={removeFromCaps} + removeFromPendingCaps={removeFromPendingCaps} onNext={handleNext} onBack={handleBack} - userRebalancerInfo={userRebalancerInfo} + account={account} /> )} {currentStep === SetupStep.Success && ( diff --git a/src/components/Avatar/Avatar.tsx b/src/components/Avatar/Avatar.tsx index fe40c82b..ea14489e 100644 --- a/src/components/Avatar/Avatar.tsx +++ b/src/components/Avatar/Avatar.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import Image from 'next/image'; import { Address } from 'viem'; -import { MORPHO } from '@/utils/morpho'; +import { getMorphoAddress } from '@/utils/morpho'; type AvatarProps = { address: Address; @@ -16,7 +16,7 @@ export function Avatar({ address, size = 30, rounded = true }: AvatarProps) { useEffect(() => { const checkEffigyAvailability = async () => { - const effigyMockurl = `https://effigy.im/a/${MORPHO}.png`; + const effigyMockurl = `https://effigy.im/a/${getMorphoAddress(1)}.png`; try { const response = await fetch(effigyMockurl, { method: 'HEAD' }); setUseEffigy(response.ok); diff --git a/src/components/WithdrawModalContent.tsx b/src/components/WithdrawModalContent.tsx index b4ad0f9d..8301456f 100644 --- a/src/components/WithdrawModalContent.tsx +++ b/src/components/WithdrawModalContent.tsx @@ -9,7 +9,8 @@ import { useMarketNetwork } from '@/hooks/useMarketNetwork'; import { useStyledToast } from '@/hooks/useStyledToast'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; import { formatBalance, formatReadable, min } from '@/utils/balance'; -import { MORPHO } from '@/utils/morpho'; +import { getMorphoAddress } from '@/utils/morpho'; +import { SupportedNetworks } from '@/utils/networks'; import { Market, MarketPosition } from '@/utils/types'; import { Button } from './common'; @@ -83,7 +84,7 @@ export function WithdrawModalContent({ sendTransaction({ account, - to: MORPHO, + to: getMorphoAddress(activeMarket.morphoBlue.chain.id as SupportedNetworks), data: encodeFunctionData({ abi: morphoAbi, functionName: 'withdraw', diff --git a/src/config/dataSources.ts b/src/config/dataSources.ts index e27d8b67..23cbe41f 100644 --- a/src/config/dataSources.ts +++ b/src/config/dataSources.ts @@ -5,12 +5,12 @@ import { SupportedNetworks } from '@/utils/networks'; */ export const getMarketDataSource = (network: SupportedNetworks): 'morpho' | 'subgraph' => { switch (network) { - // case SupportedNetworks.Mainnet: - // return 'subgraph'; - // case SupportedNetworks.Base: - // return 'subgraph'; + case SupportedNetworks.Mainnet: + return 'morpho'; + case SupportedNetworks.Base: + return 'morpho'; default: - return 'morpho'; // Default to Morpho API + return 'subgraph'; // Default to Subgraph } }; @@ -20,7 +20,11 @@ export const getMarketDataSource = (network: SupportedNetworks): 'morpho' | 'sub */ export const getHistoricalDataSource = (network: SupportedNetworks): 'morpho' | 'subgraph' => { switch (network) { - default: + case SupportedNetworks.Mainnet: return 'morpho'; + case SupportedNetworks.Base: + return 'morpho'; + default: + return 'subgraph'; } }; diff --git a/src/config/oracle-whitelist.ts b/src/config/oracle-whitelist.ts new file mode 100644 index 00000000..133cddde --- /dev/null +++ b/src/config/oracle-whitelist.ts @@ -0,0 +1,123 @@ +import { Address } from 'viem'; +import { SupportedNetworks } from '@/utils/networks'; +import { MorphoChainlinkOracleData } from '@/utils/types'; + +// Extend the oracle data structure to include optional warning codes +type WhitelistedOracleData = MorphoChainlinkOracleData & { + warningCodes?: string[]; // Array of warning codes (e.g., 'hardcoded_oracle_feed') +}; + +// Define the structure for oracle data within a specific network +type NetworkOracleWhitelist = Record; + +// Top-level map: Network ID -> Oracle Address -> Oracle Data (including warnings) +export const oracleWhitelist: { + [network in SupportedNetworks]?: NetworkOracleWhitelist; +} = { + [SupportedNetworks.Polygon]: { + '0x1dc2444b54945064c131145cd6b8701e3454c63a': { + baseFeedOne: { + address: '0x3Ea1eC855fBda8bA0396975eC260AD2e9B2Bc01c ', + chain: { id: SupportedNetworks.Polygon }, + description: 'wstETH / WETH', + id: '0x3Ea1eC855fBda8bA0396975eC260AD2e9B2Bc01c', + pair: ['wstETH', 'WETH'], + vendor: 'Chainlink', + }, + baseFeedTwo: null, + quoteFeedOne: null, + quoteFeedTwo: null, + warningCodes: ['hardcoded_oracle_feed'], + }, + '0x15b4e0ee3dc3d20d9d261da2d3e0d2a86a6a6291': { + baseFeedOne: { + address: '0xaca1222008C6Ea624522163174F80E6e17B0709A ', + chain: { id: SupportedNetworks.Polygon }, + description: 'wBTC / USD', + id: '0xaca1222008C6Ea624522163174F80E6e17B0709A', + pair: ['wBTC', 'USD'], + vendor: 'Chainlink', + }, + baseFeedTwo: null, + quoteFeedOne: null, + quoteFeedTwo: null, + warningCodes: ['hardcoded_oracle_feed'], + }, + '0xf6df1e9ac2a4239c81bde9a537236eb4b4a4828c': { + baseFeedOne: { + address: '0x66aCD49dB829005B3681E29b6F4Ba1d93843430e', + chain: { id: SupportedNetworks.Polygon }, + description: 'MATIC / USD', + id: '0x66aCD49dB829005B3681E29b6F4Ba1d93843430e', + pair: ['MATIC', 'USD'], + vendor: 'Chainlink', + }, + baseFeedTwo: null, + quoteFeedOne: null, + quoteFeedTwo: null, + warningCodes: ['hardcoded_oracle_feed'], + }, + '0x8eece0e6a57554d70f4fa35913500d4c17ac3fef': { + baseFeedOne: { + address: '0xb6Cd28DD265aBbbF24a76B47353002ffeBd56099', + chain: { id: SupportedNetworks.Polygon }, + description: 'MaticX / USD', + id: '0xb6Cd28DD265aBbbF24a76B47353002ffeBd56099', + pair: ['MaticX', 'USD'], + vendor: 'Chainlink', + }, + baseFeedTwo: null, + quoteFeedOne: null, + quoteFeedTwo: null, + warningCodes: ['hardcoded_oracle_feed'], + }, + '0xf81de2f51d33aca3b0ef672ae544d6225a0d76f2': { + baseFeedOne: { + address: '0xfBF4299519bdF63AE4296871b3a5237b09021B26', + chain: { id: SupportedNetworks.Polygon }, + description: 'ETH / USD', + id: '0xfBF4299519bdF63AE4296871b3a5237b09021B26', + pair: ['ETH', 'USD'], + vendor: 'Chainlink', + }, + baseFeedTwo: null, + quoteFeedOne: null, + quoteFeedTwo: null, + warningCodes: ['hardcoded_oracle_feed'], + }, + '0x3baefca1c626262e9140b7c789326235d9ffd16d': { + baseFeedOne: { + address: '0xaca1222008C6Ea624522163174F80E6e17B0709A', + chain: { id: SupportedNetworks.Polygon }, + description: 'WBTC / USD', + id: '0x0000000000000000000000000000000000000000', + pair: ['WBTC', 'USD'], + vendor: 'Chainlink', + }, + baseFeedTwo: null, + quoteFeedOne: { + address: '0xfBF4299519bdF63AE4296871b3a5237b09021B26', + chain: { id: SupportedNetworks.Polygon }, + description: 'ETH / USD', + id: '0xfBF4299519bdF63AE4296871b3a5237b09021B26', + pair: ['ETH', 'USD'], + vendor: 'Chainlink', + }, + quoteFeedTwo: null, + warningCodes: [], + }, + }, +}; + +/** + * Gets the whitelisted oracle data (including potential warnings) for a specific oracle address and network. + * @param oracleAddress The address of the oracle contract. + * @param network The network ID. + * @returns The WhitelistedOracleData if found, otherwise undefined. + */ +export const getWhitelistedOracleData = ( + oracleAddress: Address, + network: SupportedNetworks, +): WhitelistedOracleData | undefined => { + return oracleWhitelist[network]?.[oracleAddress]; +}; diff --git a/src/contexts/MarketsContext.tsx b/src/contexts/MarketsContext.tsx index db1bbbed..9402e9ba 100644 --- a/src/contexts/MarketsContext.tsx +++ b/src/contexts/MarketsContext.tsx @@ -59,6 +59,7 @@ export function MarketsProvider({ children }: MarketsProviderProps) { const networksToFetch: SupportedNetworks[] = [ SupportedNetworks.Mainnet, SupportedNetworks.Base, + SupportedNetworks.Polygon, ]; let combinedMarkets: Market[] = []; let fetchErrors: unknown[] = []; diff --git a/src/data-sources/subgraph/market.ts b/src/data-sources/subgraph/market.ts index 141d8fa2..cfc86f4d 100644 --- a/src/data-sources/subgraph/market.ts +++ b/src/data-sources/subgraph/market.ts @@ -1,4 +1,5 @@ import { Address, zeroAddress } from 'viem'; +import { getWhitelistedOracleData } from '@/config/oracle-whitelist'; // Import the whitelist helper import { marketQuery as subgraphMarketQuery, marketsQuery as subgraphMarketsQuery, @@ -106,6 +107,7 @@ const transformSubgraphMarketToMarket = ( const lltv = subgraphMarket.lltv ?? '0'; const irmAddress = subgraphMarket.irm ?? '0x'; const inputTokenPriceUSD = subgraphMarket.inputTokenPriceUSD ?? '0'; + const oracleAddress = (subgraphMarket.oracle?.oracleAddress ?? '0x') as Address; if ( marketId.toLowerCase() === '0x9103c3b4e834476c9a62ea009ba2c884ee42e94e6e314a26f04d312434191836' @@ -175,7 +177,23 @@ const transformSubgraphMarketToMarket = ( const supplyApy = Number(subgraphMarket.rates?.find((r) => r.side === 'LENDER')?.rate ?? 0); const borrowApy = Number(subgraphMarket.rates?.find((r) => r.side === 'BORROWER')?.rate ?? 0); - const warnings: MarketWarning[] = [SUBGRAPH_NO_ORACLE]; + let warnings: MarketWarning[] = []; // Initialize warnings + let whitelistedOracleData = getWhitelistedOracleData(oracleAddress, network); + + // Add SUBGRAPH_NO_ORACLE warning *only* if not whitelisted, or add warnings from whitelist + if (!whitelistedOracleData) { + warnings.push(SUBGRAPH_NO_ORACLE); + } else if (whitelistedOracleData.warningCodes && whitelistedOracleData.warningCodes.length > 0) { + // Add warnings specified in the whitelist configuration + const whitelistWarnings = whitelistedOracleData.warningCodes.map((code) => ({ + type: code, + // Determine level based on code if needed, or use a default/derive from code convention + // For simplicity, let's assume they are all 'warning' level for now, adjust as needed + level: 'warning', // This might need refinement based on warning code meanings + __typename: `OracleWarning_${code}`, // Construct a basic typename + })); + warnings = warnings.concat(whitelistWarnings); + } // get the prices let loanAssetPrice = safeParseFloat(subgraphMarket.borrowedToken?.lastPriceUSD ?? '0'); @@ -214,6 +232,10 @@ const transformSubgraphMarketToMarket = ( const collateralAssetsUsd = formatBalance(collateralAssets, collateralAsset.decimals) * collateralAssetPrice; + // Use whitelisted oracle data (feeds) if available, otherwise default + const oracleDataToUse = whitelistedOracleData ?? defaultOracleData; + + // Regenerate warningsWithDetail *after* potentially adding whitelist warnings const warningsWithDetail = getMarketWarningsWithDetail({ warnings }); const marketDetail: Market = { @@ -246,7 +268,7 @@ const transformSubgraphMarketToMarket = ( timestamp: timestamp, rateAtUTarget: 0, // Not available from subgraph }, - oracleAddress: subgraphMarket.oracle?.oracleAddress ?? '0x', + oracleAddress: oracleAddress, morphoBlue: { id: subgraphMarket.protocol?.id ?? '0x', address: subgraphMarket.protocol?.id ?? '0x', @@ -254,10 +276,10 @@ const transformSubgraphMarketToMarket = ( id: chainId, }, }, - warnings: warnings, + warnings: warnings, // Assign the potentially filtered warnings warningsWithDetail: warningsWithDetail, oracle: { - data: defaultOracleData, // Placeholder oracle data + data: oracleDataToUse, // Use the determined oracle data }, hasUSDPrice: hasUSDPrice, isProtectedByLiquidationBots: false, // Not available from subgraph diff --git a/src/hooks/useAuthorizeAgent.ts b/src/hooks/useAuthorizeAgent.ts index a776f988..e5ca57e9 100644 --- a/src/hooks/useAuthorizeAgent.ts +++ b/src/hooks/useAuthorizeAgent.ts @@ -5,8 +5,8 @@ import monarchAgentAbi from '@/abis/monarch-agent-v1'; import morphoAbi from '@/abis/morpho'; import { useStyledToast } from '@/hooks/useStyledToast'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; -import { AGENT_CONTRACT } from '@/utils/monarch-agent'; -import { MONARCH_TX_IDENTIFIER, MORPHO } from '@/utils/morpho'; +import { getAgentContract } from '@/utils/monarch-agent'; +import { MONARCH_TX_IDENTIFIER, getMorphoAddress } from '@/utils/morpho'; import { SupportedNetworks } from '@/utils/networks'; import { Market } from '@/utils/types'; export enum AuthorizeAgentStep { @@ -21,7 +21,7 @@ export type MarketCap = { }; /** - * This hook should only be used on Base + * This hook should only be used on Base and Polygon * @param markets * @param caps * @param onSuccess @@ -30,6 +30,7 @@ export type MarketCap = { export const useAuthorizeAgent = ( agent: Address, marketCaps: MarketCap[], + targetChainId: SupportedNetworks, onSuccess?: () => void, ) => { const toast = useStyledToast(); @@ -39,21 +40,25 @@ export const useAuthorizeAgent = ( const { switchChainAsync } = useSwitchChain(); const { address: account, chainId } = useAccount(); + const { signTypedDataAsync } = useSignTypedData(); + const AGENT_CONTRACT = getAgentContract(targetChainId); + const { data: isAuthorized } = useReadContract({ - address: MORPHO, + address: getMorphoAddress(targetChainId), abi: morphoAbi, functionName: 'isAuthorized', + chainId: targetChainId, args: [account as Address, AGENT_CONTRACT], }); const { data: nonce } = useReadContract({ - address: MORPHO, + address: getMorphoAddress(targetChainId), abi: morphoAbi, functionName: 'nonce', args: [account as Address], - chainId: SupportedNetworks.Base, + chainId: targetChainId, }); const { data: rebalancerAddress } = useReadContract({ @@ -61,7 +66,7 @@ export const useAuthorizeAgent = ( abi: monarchAgentAbi, functionName: 'rebalancers', args: [account as Address], - chainId: SupportedNetworks.Base, + chainId: targetChainId, query: { enabled: !!account }, }); @@ -70,7 +75,7 @@ export const useAuthorizeAgent = ( pendingText: 'Auhorizing Monarch Agent', successText: 'Monarch Agent authorized successfully', errorText: 'Failed to authorize Monarch Agent', - chainId: SupportedNetworks.Base, + chainId: targetChainId, onSuccess, }); @@ -80,8 +85,8 @@ export const useAuthorizeAgent = ( return; } - if (chainId !== SupportedNetworks.Base) { - await switchChainAsync({ chainId: SupportedNetworks.Base }); + if (chainId !== targetChainId) { + await switchChainAsync({ chainId: targetChainId }); } setIsConfirming(true); @@ -94,8 +99,8 @@ export const useAuthorizeAgent = ( setCurrentStep(AuthorizeAgentStep.Authorize); if (isAuthorized === false) { const domain = { - chainId: SupportedNetworks.Base, - verifyingContract: MORPHO as Address, + chainId: targetChainId, + verifyingContract: getMorphoAddress(targetChainId) as Address, }; const types = { @@ -195,7 +200,7 @@ export const useAuthorizeAgent = ( account, to: AGENT_CONTRACT, data: multicallTx, - chainId: SupportedNetworks.Base, + chainId: targetChainId, }); } catch (error) { console.error('Error during agent setup:', error); @@ -220,6 +225,7 @@ export const useAuthorizeAgent = ( rebalancerAddress, chainId, switchChainAsync, + targetChainId, ], ); diff --git a/src/hooks/useMorphoBundlerAuthorization.ts b/src/hooks/useMorphoBundlerAuthorization.ts index 13c0c465..5ad78c32 100644 --- a/src/hooks/useMorphoBundlerAuthorization.ts +++ b/src/hooks/useMorphoBundlerAuthorization.ts @@ -4,7 +4,7 @@ import { useAccount, useReadContract, useSignTypedData } from 'wagmi'; import morphoBundlerAbi from '@/abis/bundlerV2'; import morphoAbi from '@/abis/morpho'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; -import { MORPHO } from '@/utils/morpho'; +import { getMorphoAddress } from '@/utils/morpho'; import { useStyledToast } from './useStyledToast'; type UseMorphoBundlerAuthorizationProps = { @@ -22,7 +22,7 @@ export const useMorphoBundlerAuthorization = ({ const [isAuthorizing, setIsAuthorizing] = useState(false); const { data: isBundlerAuthorized, refetch: refetchIsBundlerAuthorized } = useReadContract({ - address: MORPHO, + address: getMorphoAddress(chainId), abi: morphoAbi, functionName: 'isAuthorized', args: [account as Address, bundlerAddress], @@ -33,7 +33,7 @@ export const useMorphoBundlerAuthorization = ({ }); const { data: nonce, refetch: refetchNonce } = useReadContract({ - address: MORPHO, + address: getMorphoAddress(chainId), abi: morphoAbi, functionName: 'nonce', args: [account as Address], @@ -70,7 +70,7 @@ export const useMorphoBundlerAuthorization = ({ try { const domain = { chainId: chainId, - verifyingContract: MORPHO as Address, + verifyingContract: getMorphoAddress(chainId) as Address, }; const types = { @@ -158,7 +158,7 @@ export const useMorphoBundlerAuthorization = ({ // Simple Morpho setAuthorization transaction await sendBundlerAuthorizationTx({ account: account, - to: MORPHO, + to: getMorphoAddress(chainId), data: encodeFunctionData({ abi: morphoAbi, functionName: 'setAuthorization', diff --git a/src/hooks/useSupplyMarket.ts b/src/hooks/useSupplyMarket.ts index e56a2e56..fa270dd3 100644 --- a/src/hooks/useSupplyMarket.ts +++ b/src/hooks/useSupplyMarket.ts @@ -62,6 +62,8 @@ export function useSupplyMarket(market: Market, onSuccess?: () => void): UseSupp chainId: market.morphoBlue.chain.id, }); + console.log('tokenBalance', tokenBalance); + // Get ETH balance const { data: ethBalance } = useBalance({ address: account, diff --git a/src/hooks/useUserBalances.ts b/src/hooks/useUserBalances.ts index bbc7c688..52d94800 100644 --- a/src/hooks/useUserBalances.ts +++ b/src/hooks/useUserBalances.ts @@ -51,9 +51,10 @@ export function useUserBalances() { try { // Fetch balances from both chains - const [mainnetBalances, baseBalances] = await Promise.all([ + const [mainnetBalances, baseBalances, polygonBalances] = await Promise.all([ fetchBalances(SupportedNetworks.Mainnet), fetchBalances(SupportedNetworks.Base), + fetchBalances(SupportedNetworks.Polygon), ]); // Process and filter tokens @@ -77,7 +78,7 @@ export function useUserBalances() { processTokens(mainnetBalances, 1); processTokens(baseBalances, 8453); - + processTokens(polygonBalances, 137); setBalances(processedBalances); } catch (err) { setError(err instanceof Error ? err : new Error('Unknown error occurred')); diff --git a/src/hooks/useUserPositionsSummaryData.ts b/src/hooks/useUserPositionsSummaryData.ts index 2a8eaac0..238408d3 100644 --- a/src/hooks/useUserPositionsSummaryData.ts +++ b/src/hooks/useUserPositionsSummaryData.ts @@ -176,7 +176,6 @@ const useUserPositionsSummaryData = (user: string | undefined) => { // Update hasInitialData when we first get positions with earnings useEffect(() => { if (positionsWithEarnings && positionsWithEarnings.fetched && !hasInitialData) { - console.log('positionsWithEarnings', positionsWithEarnings); setHasInitialData(true); } }, [positionsWithEarnings, hasInitialData]); @@ -196,12 +195,9 @@ const useUserPositionsSummaryData = (user: string | undefined) => { // 1. We haven't received initial data yet // 2. Positions are still loading initially // 3. We have positions but no earnings data yet - const isPositionsLoading = !hasInitialData || positionsLoading || !positionsWithEarnings?.fetched; + const isPositionsLoading = + !hasInitialData || positionsLoading || (!!positions?.length && !positionsWithEarnings?.fetched); - // Consider earnings loading if: - // 1. Block numbers are loading - // 2. Initial earnings query is loading - // 3. Earnings are being fetched/calculated (even if we have placeholder data) const isEarningsLoading = isLoadingBlockNums || isLoadingEarningsQuery || isFetchingEarnings; return { diff --git a/src/hooks/useUserRebalancerInfo.ts b/src/hooks/useUserRebalancerInfo.ts index 2de737ae..8036c618 100644 --- a/src/hooks/useUserRebalancerInfo.ts +++ b/src/hooks/useUserRebalancerInfo.ts @@ -1,41 +1,61 @@ import { useState, useEffect, useCallback } from 'react'; import { userRebalancerInfoQuery } from '@/graphql/morpho-api-queries'; +import { agentNetworks } from '@/utils/networks'; import { UserRebalancerInfo } from '@/utils/types'; -import { URLS } from '@/utils/urls'; +import { getMonarchAgentUrl } from '@/utils/urls'; export function useUserRebalancerInfo(account: string | undefined) { const [loading, setLoading] = useState(true); - const [data, setData] = useState(); + const [data, setData] = useState([]); const [error, setError] = useState(null); const fetchData = useCallback(async () => { if (!account) { setLoading(false); + setData([]); return; } try { setLoading(true); - const response = await fetch(URLS.MONARCH_AGENT_API, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query: userRebalancerInfoQuery, - variables: { id: account.toLowerCase() }, - }), + setError(null); + + const promises = agentNetworks.map(async (networkId) => { + const apiUrl = getMonarchAgentUrl(networkId); + if (!apiUrl) return null; + + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: userRebalancerInfoQuery, + variables: { id: account.toLowerCase() }, + }), + }); + + const json = (await response.json()) as { data?: { user?: UserRebalancerInfo } }; + + if (json.data?.user) { + return { + ...json.data.user, + network: networkId, + } as UserRebalancerInfo; + } + return null; }); - const json = (await response.json()) as { data?: { user?: UserRebalancerInfo } }; + const results = await Promise.all(promises); + const validResults = results.filter( + (result): result is UserRebalancerInfo => result !== null, + ); - if (json.data?.user) { - setData(json.data.user); - } - setError(null); + setData(validResults); } catch (err) { console.error('Error fetching rebalancer info:', err); setError(err); + setData([]); } finally { setLoading(false); } @@ -46,7 +66,7 @@ export function useUserRebalancerInfo(account: string | undefined) { }, [fetchData]); return { - rebalancerInfo: data, + rebalancerInfos: data, loading, error, refetch: fetchData, diff --git a/src/imgs/chains/polygon.png b/src/imgs/chains/polygon.png new file mode 100644 index 00000000..e4e4a8a0 Binary files /dev/null and b/src/imgs/chains/polygon.png differ diff --git a/src/imgs/tokens/maticx.png b/src/imgs/tokens/maticx.png new file mode 100644 index 00000000..6a33317f Binary files /dev/null and b/src/imgs/tokens/maticx.png differ diff --git a/src/imgs/tokens/wpol.webp b/src/imgs/tokens/wpol.webp new file mode 100644 index 00000000..acc75ce4 Binary files /dev/null and b/src/imgs/tokens/wpol.webp differ diff --git a/src/services/statsService.ts b/src/services/statsService.ts index ef02bc7a..1a4de0ba 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -1,53 +1,33 @@ import { request, gql } from 'graphql-request'; -import { transactionsByTimeRangeQuery, userGrowthQuery } from '@/graphql/monarch-stats-queries'; +import { transactionsByTimeRangeQuery } from '@/graphql/monarch-stats-queries'; import { SupportedNetworks } from '@/utils/networks'; import { processTransactionData } from '@/utils/statsDataProcessing'; import { TimeFrame, - MetricPeriod, Transaction, - TimeSeriesData, AssetVolumeData, PlatformStats, getTimeRange, getPreviousTimeRange, - groupTransactionsByPeriod, calculatePlatformStats, } from '@/utils/statsUtils'; import { supportedTokens } from '@/utils/tokens'; -// Default API endpoints for different networks -const DEFAULT_API_ENDPOINTS = { - [SupportedNetworks.Base]: - process.env.NEXT_PUBLIC_BASE_METRICS_API ?? - 'https://api.studio.thegraph.com/query/94369/monarch-metrics/version/latest', - [SupportedNetworks.Mainnet]: - process.env.NEXT_PUBLIC_MAINNET_METRICS_API ?? - 'https://api.thegraph.com/subgraphs/name/monarch/metrics-mainnet', -}; - // GraphQL response types type TransactionResponse = { userTransactions: Transaction[]; }; -type UserGrowthResponse = { - users: { id: string; firstTxTimestamp: string }[]; -}; - /** * Fetch transactions for a specific time range */ export const fetchTransactionsByTimeRange = async ( startTime: number, endTime: number, - networkId: SupportedNetworks = SupportedNetworks.Base, - apiEndpoint?: string, + networkId: SupportedNetworks, + endpoint: string, ): Promise => { try { - // Get the API endpoint for the selected network - const endpoint = apiEndpoint ?? DEFAULT_API_ENDPOINTS[networkId]; - console.log( `Fetching transactions between ${new Date(startTime * 1000).toISOString()} and ${new Date( endTime * 1000, @@ -132,147 +112,13 @@ export const fetchTransactionsByTimeRange = async ( } }; -/** - * Fetch user growth data - */ -export const fetchUserGrowth = async ( - timeframe: TimeFrame, - period: MetricPeriod, - networkId: SupportedNetworks = SupportedNetworks.Base, - apiEndpoint?: string, -): Promise => { - try { - const { startTime, endTime } = getTimeRange(timeframe); - - // Get the API endpoint for the selected network - const endpoint = apiEndpoint ?? DEFAULT_API_ENDPOINTS[networkId]; - - console.log( - `Fetching user growth between ${new Date(startTime * 1000).toISOString()} and ${new Date( - endTime * 1000, - ).toISOString()}`, - ); - console.log(`Using API endpoint: ${endpoint}`); - - const batchSize = 1000; - let skip = 0; - let allUsers: { id: string; firstTxTimestamp: string }[] = []; - let hasMore = true; - - // Paginate through all available users - while (hasMore) { - const variables = { - startTime: startTime.toString(), - endTime: endTime.toString(), - first: batchSize, - skip: skip, - }; - - console.log(`Fetching users batch: first=${batchSize}, skip=${skip}`); - - // Fetch from specified network - const response = await request( - endpoint, - gql` - ${userGrowthQuery} - `, - variables, - ).catch((error) => { - console.warn(`Error fetching user growth from network ${networkId}:`, error); - return { users: [] }; - }); - - const users = response.users ?? []; - - console.log(`Found ${users.length} users in batch (skip=${skip})`); - - // Add to our collection - allUsers = [...allUsers, ...users]; - - // Check if we should fetch more - if (users.length < batchSize) { - hasMore = false; - } else { - skip += batchSize; - } - } - - console.log(`Found a total of ${allUsers.length} users after pagination`); - - // Group users by their first transaction date - const usersByDate: Record = {}; - - allUsers.forEach((user) => { - const date = new Date(Number(user.firstTxTimestamp) * 1000); - let periodKey: string; - - switch (period) { - case 'daily': - periodKey = date.toISOString().split('T')[0]; - break; - case 'weekly': - const weekStart = new Date(date); - weekStart.setDate(date.getDate() - date.getDay()); - periodKey = weekStart.toISOString().split('T')[0]; - break; - case 'monthly': - periodKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; - break; - } - - if (!usersByDate[periodKey]) { - usersByDate[periodKey] = 0; - } - - usersByDate[periodKey]++; - }); - - // Convert to time series data format - return Object.entries(usersByDate) - .map(([date, value]) => ({ date, value })) - .sort((a, b) => a.date.localeCompare(b.date)); - } catch (error) { - console.error('Error fetching user growth:', error); - return []; - } -}; - -/** - * Fetch volume data over time - */ -export const fetchVolumeOverTime = async ( - timeframe: TimeFrame, - period: MetricPeriod, - tokenDecimals = 18, - networkId: SupportedNetworks = SupportedNetworks.Base, - apiEndpoint?: string, -): Promise => { - try { - console.log( - `Fetching volume data for timeframe: ${timeframe}, period: ${period} from network ${networkId}`, - ); - const { startTime, endTime } = getTimeRange(timeframe); - const transactions = await fetchTransactionsByTimeRange( - startTime, - endTime, - networkId, - apiEndpoint, - ); - - return groupTransactionsByPeriod(transactions, period, tokenDecimals); - } catch (error) { - console.error('Error fetching volume over time:', error); - return []; - } -}; - /** * Fetch and calculate platform-wide statistics */ export const fetchPlatformStats = async ( timeframe: TimeFrame, - networkId: SupportedNetworks = SupportedNetworks.Base, - apiEndpoint?: string, + networkId: SupportedNetworks, + endpoint: string, ): Promise => { try { console.log(`Fetching platform stats for timeframe: ${timeframe} from network ${networkId}`); @@ -284,13 +130,13 @@ export const fetchPlatformStats = async ( currentRange.startTime, currentRange.endTime, networkId, - apiEndpoint, + endpoint, ), fetchTransactionsByTimeRange( previousRange.startTime, previousRange.endTime, networkId, - apiEndpoint, + endpoint, ), ]); @@ -316,8 +162,8 @@ export const fetchPlatformStats = async ( */ export const fetchAssetMetrics = async ( timeframe: TimeFrame, - networkId: SupportedNetworks = SupportedNetworks.Base, - apiEndpoint?: string, + networkId: SupportedNetworks, + endpoint: string, ): Promise => { try { console.log(`Fetching asset metrics for timeframe: ${timeframe} from network ${networkId}`); @@ -326,7 +172,7 @@ export const fetchAssetMetrics = async ( startTime, endTime, networkId, - apiEndpoint, + endpoint, ); console.log(`Processing ${transactions.length} transactions for asset metrics`); @@ -393,9 +239,9 @@ export const fetchAssetMetrics = async ( * Build a statistics payload with all relevant metrics */ export const fetchAllStatistics = async ( + networkId: SupportedNetworks, + endpoint: string, timeframe: TimeFrame = '30D', - networkId: SupportedNetworks = SupportedNetworks.Base, - apiEndpoint?: string, ): Promise<{ platformStats: PlatformStats; assetMetrics: AssetVolumeData[]; @@ -405,8 +251,8 @@ export const fetchAllStatistics = async ( const startTime = performance.now(); const [platformStats, assetMetrics] = await Promise.all([ - fetchPlatformStats(timeframe, networkId, apiEndpoint), - fetchAssetMetrics(timeframe, networkId, apiEndpoint), + fetchPlatformStats(timeframe, networkId, endpoint), + fetchAssetMetrics(timeframe, networkId, endpoint), ]); const endTime = performance.now(); diff --git a/src/store/createWagmiConfig.ts b/src/store/createWagmiConfig.ts index 4a5baa8b..4fe889dd 100644 --- a/src/store/createWagmiConfig.ts +++ b/src/store/createWagmiConfig.ts @@ -11,13 +11,14 @@ import { } from '@rainbow-me/rainbowkit/wallets'; import { safe } from '@wagmi/connectors'; import { createConfig, http } from 'wagmi'; -import { base, mainnet } from 'wagmi/chains'; +import { base, mainnet, polygon } from 'wagmi/chains'; import { getChainsForEnvironment } from './supportedChains'; const alchemyKey = process.env.NEXT_PUBLIC_ALCHEMY_API_KEY; const rpcMainnet = `https://eth-mainnet.g.alchemy.com/v2/${alchemyKey}`; const rpcBase = `https://base-mainnet.g.alchemy.com/v2/${alchemyKey}`; +const rpcPolygon = `https://polygon-mainnet.g.alchemy.com/v2/${alchemyKey}`; export function createWagmiConfig(projectId: string) { const connectors = connectorsForWallets( @@ -51,6 +52,7 @@ export function createWagmiConfig(projectId: string) { transports: { [mainnet.id]: http(rpcMainnet), [base.id]: http(rpcBase), + [polygon.id]: http(rpcPolygon), }, connectors: [ ...connectors, diff --git a/src/store/supportedChains.ts b/src/store/supportedChains.ts index f623bbed..5f208910 100644 --- a/src/store/supportedChains.ts +++ b/src/store/supportedChains.ts @@ -1,12 +1,12 @@ -import { base, Chain, mainnet } from 'viem/chains'; +import { base, Chain, mainnet, polygon } from 'viem/chains'; import { Environment, getCurrentEnvironment } from './environment'; // The list of supported Chains for a given environment export const SUPPORTED_CHAINS: Record = { - [Environment.localhost]: [mainnet, base], - [Environment.development]: [mainnet, base], - [Environment.staging]: [mainnet, base], - [Environment.production]: [mainnet, base], + [Environment.localhost]: [mainnet, base, polygon], + [Environment.development]: [mainnet, base, polygon], + [Environment.staging]: [mainnet, base, polygon], + [Environment.production]: [mainnet, base, polygon], }; /** diff --git a/src/utils/external.ts b/src/utils/external.ts index 60d10a03..43942f8b 100644 --- a/src/utils/external.ts +++ b/src/utils/external.ts @@ -9,6 +9,8 @@ export const getAssetURL = (address: string, chain: SupportedNetworks): string = switch (chain) { case SupportedNetworks.Base: return `https://basescan.org/token/${address}`; + case SupportedNetworks.Polygon: + return `https://polygonscan.com/token/${address}`; default: return `https://etherscan.io/token/${address}`; } @@ -18,6 +20,8 @@ export const getExplorerURL = (address: string, chain: SupportedNetworks): strin switch (chain) { case SupportedNetworks.Base: return `https://basescan.org/address/${address}`; + case SupportedNetworks.Polygon: + return `https://polygonscan.com/address/${address}`; default: return `https://etherscan.io/address/${address}`; } @@ -27,6 +31,8 @@ export const getExplorerTxURL = (hash: string, chain: SupportedNetworks): string switch (chain) { case SupportedNetworks.Base: return `https://basescan.org/tx/${hash}`; + case SupportedNetworks.Polygon: + return `https://polygonscan.com/tx/${hash}`; default: return `https://etherscan.io/tx/${hash}`; } diff --git a/src/utils/monarch-agent.ts b/src/utils/monarch-agent.ts index e4ddc17c..446d059d 100644 --- a/src/utils/monarch-agent.ts +++ b/src/utils/monarch-agent.ts @@ -1,6 +1,17 @@ +import { zeroAddress } from 'viem'; +import { SupportedNetworks } from './networks'; import { AgentMetadata } from './types'; -export const AGENT_CONTRACT = '0x6a9BA5c91fDd608b3F85c3E031a4f531f331f545'; +export const getAgentContract = (chain: SupportedNetworks) => { + switch (chain) { + case SupportedNetworks.Base: + return '0x6a9BA5c91fDd608b3F85c3E031a4f531f331f545'; + case SupportedNetworks.Polygon: + return '0x01c90eEb82f982301fE4bd11e36A5704673CF18C'; + default: + return zeroAddress; + } +}; export enum KnownAgents { MAX_APY = '0xe0e04468A54937244BEc3bc6C1CA8Bc36ECE6704', diff --git a/src/utils/morpho.ts b/src/utils/morpho.ts index 40e80643..ed3ace35 100644 --- a/src/utils/morpho.ts +++ b/src/utils/morpho.ts @@ -1,19 +1,39 @@ +import { zeroAddress } from 'viem'; import { SupportedNetworks } from './networks'; import { UserTxTypes } from './types'; -export const MORPHO = '0xbbbbbbbbbb9cc5e90e3b3af64bdaf62c37eeffcb'; +// export const MORPHO = '0xbbbbbbbbbb9cc5e90e3b3af64bdaf62c37eeffcb'; // appended to the end of datahash to identify a monarch tx export const MONARCH_TX_IDENTIFIER = 'beef'; -export const getBundlerV2 = (chain: SupportedNetworks) => { - if (chain === SupportedNetworks.Base) { - // ChainAgnosticBundlerV2 - return '0x23055618898e202386e6c13955a58D3C68200BFB'; +export const getMorphoAddress = (chain: SupportedNetworks) => { + switch (chain) { + case SupportedNetworks.Mainnet: + return '0xbbbbbbbbbb9cc5e90e3b3af64bdaf62c37eeffcb'; + case SupportedNetworks.Base: + return '0xbbbbbbbbbb9cc5e90e3b3af64bdaf62c37eeffcb'; + case SupportedNetworks.Polygon: + return '0x1bf0c2541f820e775182832f06c0b7fc27a25f67'; + default: + return zeroAddress; } +}; + +export const getBundlerV2 = (chain: SupportedNetworks) => { + switch (chain) { + case SupportedNetworks.Mainnet: + return '0x4095F064B8d3c3548A3bebfd0Bbfd04750E30077'; + case SupportedNetworks.Base: + // ChainAgnosticBundlerV2 + return '0x23055618898e202386e6c13955a58D3C68200BFB'; + case SupportedNetworks.Polygon: + // ChainAgnosticBundlerV2 + return '0x5738366B9348f22607294007e75114922dF2a16A'; - // EthereumBundlerV2 - return '0x4095F064B8d3c3548A3bebfd0Bbfd04750E30077'; + default: + return zeroAddress; + } }; export const getIRMTitle = (address: string) => { @@ -22,6 +42,8 @@ export const getIRMTitle = (address: string) => { return 'Adaptive Curve'; case '0x46415998764c29ab2a25cbea6254146d50d22687': // on base return 'Adaptive Curve'; + case '0xe675a2161d4a6e2de2eed70ac98eebf257fbf0b0': // on polygon + return 'Adaptive Curve'; default: return 'Unknown IRM'; } @@ -42,13 +64,16 @@ export const actionTypeToText = (type: UserTxTypes) => { const MAINNET_GENESIS_DATE = new Date('2023-12-28T09:09:23.000Z'); const BASE_GENESIS_DATE = new Date('2024-05-03T13:40:43.000Z'); +const POLYGON_GENESIS_DATE = new Date('2025-01-20T02:03:12.000Z'); export function getMorphoGenesisDate(chainId: number): Date { switch (chainId) { - case 1: // mainnet + case SupportedNetworks.Mainnet: // mainnet return MAINNET_GENESIS_DATE; - case 8453: // base + case SupportedNetworks.Base: // base return BASE_GENESIS_DATE; + case SupportedNetworks.Polygon: + return POLYGON_GENESIS_DATE; default: return MAINNET_GENESIS_DATE; // default to mainnet } diff --git a/src/utils/networks.ts b/src/utils/networks.ts index 3ac00ac8..ac4af2af 100644 --- a/src/utils/networks.ts +++ b/src/utils/networks.ts @@ -1,12 +1,19 @@ enum SupportedNetworks { Mainnet = 1, Base = 8453, + Polygon = 137, } const isSupportedChain = (chainId: number) => { return Object.values(SupportedNetworks).includes(chainId); }; +const agentNetworks = [SupportedNetworks.Base, SupportedNetworks.Polygon]; + +const isAgentAvailable = (chainId: number) => { + return agentNetworks.includes(chainId); +}; + const networks = [ { network: SupportedNetworks.Mainnet, @@ -18,6 +25,11 @@ const networks = [ logo: require('../imgs/chains/base.webp') as string, name: 'Base', }, + { + network: SupportedNetworks.Polygon, + logo: require('../imgs/chains/polygon.png') as string, + name: 'Polygon', + }, ]; const getNetworkImg = (chainId: number) => { @@ -30,4 +42,12 @@ const getNetworkName = (chainId: number) => { return target?.name; }; -export { SupportedNetworks, isSupportedChain, getNetworkImg, getNetworkName, networks }; +export { + SupportedNetworks, + isSupportedChain, + getNetworkImg, + getNetworkName, + networks, + isAgentAvailable, + agentNetworks, +}; diff --git a/src/utils/positions.ts b/src/utils/positions.ts index b8725f78..d9cfa999 100644 --- a/src/utils/positions.ts +++ b/src/utils/positions.ts @@ -1,5 +1,4 @@ -import { Address } from 'viem'; -import { formatBalance } from './balance'; +import { Address, formatUnits } from 'viem'; import { calculateEarningsFromSnapshot } from './interest'; import { SupportedNetworks } from './networks'; import { @@ -9,6 +8,7 @@ import { UserTransaction, GroupedPosition, WarningWithDetail, + UserRebalancerInfo, } from './types'; export type PositionSnapshot = { @@ -226,19 +226,23 @@ export function getGroupedEarnings( * Group positions by loan asset * * @param positions - Array of positions with earnings - * @param rebalancerInfo - Optional rebalancer info + * @param rebalancerInfos - Array of rebalancer info objects for different networks * @returns Array of grouped positions */ export function groupPositionsByLoanAsset( positions: MarketPositionWithEarnings[], - rebalancerInfo?: { marketCaps: { marketId: string }[] }, + rebalancerInfos: UserRebalancerInfo[] = [], ): GroupedPosition[] { return positions - .filter( - (position) => + .filter((position) => { + const networkRebalancerInfo = rebalancerInfos.find( + (info) => info.network === position.market.morphoBlue.chain.id, + ); + return ( BigInt(position.state.supplyShares) > 0 || - rebalancerInfo?.marketCaps.some((c) => c.marketId === position.market.uniqueKey), - ) + networkRebalancerInfo?.marketCaps.some((c) => c.marketId === position.market.uniqueKey) + ); + }) .reduce((acc: GroupedPosition[], position) => { const loanAssetAddress = position.market.loanAsset.address; const loanAssetDecimals = position.market.loanAsset.decimals; @@ -265,49 +269,68 @@ export function groupPositionsByLoanAsset( acc.push(groupedPosition); } - // only push if the position has > 0 supply, earning or is in rebalancer info - if ( - Number(position.state.supplyShares) === 0 && - !rebalancerInfo?.marketCaps.some((c) => c.marketId === position.market.uniqueKey) - ) { - return acc; - } - - groupedPosition.markets.push(position); - - groupedPosition.allWarnings = [ - ...new Set([...groupedPosition.allWarnings, ...(position.market.warningsWithDetail || [])]), - ] as WarningWithDetail[]; - - const supplyAmount = Number( - formatBalance(position.state.supplyAssets, position.market.loanAsset.decimals), + const networkRebalancerInfoForAdd = rebalancerInfos.find( + (info) => info.network === position.market.morphoBlue.chain.id, ); - groupedPosition.totalSupply += supplyAmount; - const weightedApy = supplyAmount * position.market.state.supplyApy; - groupedPosition.totalWeightedApy += weightedApy; + // Check if position should be included in the group + const shouldInclude = + BigInt(position.state.supplyShares) > 0 || + getEarningsForPeriod(position, EarningsPeriod.All) !== '0' || + networkRebalancerInfoForAdd?.marketCaps.some( + (c) => c.marketId === position.market.uniqueKey, + ); + + if (shouldInclude) { + groupedPosition.markets.push(position); - const collateralAddress = position.market.collateralAsset?.address; - const collateralSymbol = position.market.collateralAsset?.symbol; + // Restore original logic for totals, warnings, and collaterals + groupedPosition.allWarnings = [ + ...new Set([ + ...groupedPosition.allWarnings, + ...(position.market.warningsWithDetail || []), + ]), + ] as WarningWithDetail[]; - if (collateralAddress && collateralSymbol) { - const existingCollateral = groupedPosition.collaterals.find( - (c) => c.address === collateralAddress, + const supplyAmount = Number( + formatUnits(BigInt(position.state.supplyAssets), loanAssetDecimals), ); - if (existingCollateral) { - existingCollateral.amount += supplyAmount; - } else { - groupedPosition.collaterals.push({ - address: collateralAddress, - symbol: collateralSymbol, - amount: supplyAmount, - }); + groupedPosition.totalSupply += supplyAmount; + + const weightedApyContribution = supplyAmount * (position.market.state?.supplyApy ?? 0); // Use optional chaining for state + groupedPosition.totalWeightedApy += weightedApyContribution; // Accumulate weighted APY sum + + const collateralAddress = position.market.collateralAsset?.address; + const collateralSymbol = position.market.collateralAsset?.symbol; + + if (collateralAddress && collateralSymbol) { + const existingCollateral = groupedPosition.collaterals.find( + (c) => c.address === collateralAddress, + ); + if (existingCollateral) { + existingCollateral.amount += supplyAmount; + } else { + groupedPosition.collaterals.push({ + address: collateralAddress, + symbol: collateralSymbol, + amount: supplyAmount, + }); + } } } return acc; }, []) - .filter((groupedPosition) => groupedPosition.totalSupply > 0) + .map((groupedPosition) => { + // Calculate the final average weighted APY + if (groupedPosition.totalSupply > 0) { + groupedPosition.totalWeightedApy = + groupedPosition.totalWeightedApy / groupedPosition.totalSupply; + } else { + groupedPosition.totalWeightedApy = 0; // Avoid division by zero + } + return groupedPosition; + }) .sort((a, b) => b.totalSupply - a.totalSupply); } diff --git a/src/utils/rpc.ts b/src/utils/rpc.ts index b85afd64..183d98fe 100644 --- a/src/utils/rpc.ts +++ b/src/utils/rpc.ts @@ -1,5 +1,5 @@ import { createPublicClient, http } from 'viem'; -import { base, mainnet } from 'viem/chains'; +import { base, mainnet, polygon } from 'viem/chains'; import { SupportedNetworks } from './networks'; // Initialize Alchemy clients for each chain @@ -13,19 +13,40 @@ export const baseClient = createPublicClient({ transport: http(`https://base-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`), }); +export const polygonClient = createPublicClient({ + chain: polygon, + transport: http(`https://polygon-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`), +}); + +export const getClient = (chainId: SupportedNetworks) => { + switch (chainId) { + case SupportedNetworks.Mainnet: + return mainnetClient; + case SupportedNetworks.Base: + return baseClient; + case SupportedNetworks.Polygon: + return polygonClient; + default: + throw new Error(`Unsupported chainId: ${chainId}`); + } +}; + export const BLOCK_TIME = { [SupportedNetworks.Mainnet]: 12, // Ethereum mainnet: 12 seconds [SupportedNetworks.Base]: 2, // Base: 2 seconds + [SupportedNetworks.Polygon]: 2, // Polygon: 2 seconds } as const; export const GENESIS_BLOCK = { [SupportedNetworks.Mainnet]: 18883124, // Ethereum mainnet [SupportedNetworks.Base]: 13977148, // Base + [SupportedNetworks.Polygon]: 66931042, // Polygon } as const; export const LATEST_BLOCK_DELAY = { [SupportedNetworks.Mainnet]: 0, // Ethereum mainnet [SupportedNetworks.Base]: 20, // Base + [SupportedNetworks.Polygon]: 20, // Polygon }; type BlockResponse = { diff --git a/src/utils/storageKeys.ts b/src/utils/storageKeys.ts index 81164e7c..e73cb097 100644 --- a/src/utils/storageKeys.ts +++ b/src/utils/storageKeys.ts @@ -7,6 +7,9 @@ export const MarketEntriesPerPageKey = 'monarch_marketsEntriesPerPage'; export const MarketsShowUnknownKey = 'monarch_marketsShowUnknown'; export const MarketsShowUnknownOracleKey = 'monarch_marketsShowUnknownOracle'; +export const PositionsShowEmptyKey = 'positions:show-empty'; +export const PositionsShowCollateralExposureKey = 'positions:show-collateral-exposure'; + export const ThemeKey = 'theme'; export const CacheMarketPositionKeys = 'monarch_cache_market_unique_keys'; diff --git a/src/utils/subgraph-urls.ts b/src/utils/subgraph-urls.ts index 75fc493b..d5d66807 100644 --- a/src/utils/subgraph-urls.ts +++ b/src/utils/subgraph-urls.ts @@ -17,11 +17,15 @@ const mainnetSubgraphUrl = apiKey ? `https://gateway.thegraph.com/api/${apiKey}/subgraphs/id/8Lz789DP5VKLXumTMTgygjU2xtuzx8AhbaacgN5PYCAs` : undefined; +const polygonSubgraphUrl = apiKey + ? `https://gateway.thegraph.com/api/${apiKey}/subgraphs/id/EhFokmwryNs7qbvostceRqVdjc3petuD13mmdUiMBw8Y` + : undefined; + // Map network IDs (from SupportedNetworks) to Subgraph URLs export const SUBGRAPH_URLS: { [key in SupportedNetworks]?: string } = { [SupportedNetworks.Base]: baseSubgraphUrl, [SupportedNetworks.Mainnet]: mainnetSubgraphUrl, - // Add other supported networks and their Subgraph URLs here + [SupportedNetworks.Polygon]: polygonSubgraphUrl, }; export const getSubgraphUrl = (network: SupportedNetworks): string | undefined => { diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts index 8d89cdf7..0f241d6d 100644 --- a/src/utils/tokens.ts +++ b/src/utils/tokens.ts @@ -1,4 +1,4 @@ -import { Chain, base, mainnet } from 'viem/chains'; +import { Chain, base, mainnet, polygon } from 'viem/chains'; import { SupportedNetworks } from './networks'; export type SingleChainERC20Basic = { @@ -51,6 +51,7 @@ const supportedTokens = [ networks: [ { chain: mainnet, address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' }, { chain: base, address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' }, + { chain: polygon, address: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359' }, ], peg: TokenPeg.USD, }, @@ -58,7 +59,10 @@ const supportedTokens = [ symbol: 'USDT', img: require('../imgs/tokens/usdt.webp') as string, decimals: 6, - networks: [{ chain: mainnet, address: '0xdac17f958d2ee523a2206206994597c13d831ec7' }], + networks: [ + { chain: mainnet, address: '0xdac17f958d2ee523a2206206994597c13d831ec7' }, + { chain: polygon, address: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f' }, + ], peg: TokenPeg.USD, }, { @@ -195,9 +199,20 @@ const supportedTokens = [ networks: [ { chain: mainnet, address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' }, { chain: base, address: '0x4200000000000000000000000000000000000006' }, + + // wrapped eth on polygon, defined here as it will not be interpreted as "WETH Contract" + // which is determined by isWETH function + // This is solely for displaying and linking to eth. + { chain: polygon, address: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619' }, ], peg: TokenPeg.ETH, }, + { + symbol: 'WMATIC', + img: require('../imgs/tokens/wpol.webp') as string, + decimals: 18, + networks: [{ chain: polygon, address: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270' }], + }, { symbol: 'sDAI', img: require('../imgs/tokens/sdai.svg') as string, @@ -252,7 +267,10 @@ const supportedTokens = [ symbol: 'WBTC', img: require('../imgs/tokens/wbtc.png') as string, decimals: 8, - networks: [{ chain: mainnet, address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599' }], + networks: [ + { chain: mainnet, address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599' }, + { chain: polygon, address: '0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6' }, + ], peg: TokenPeg.BTC, }, { @@ -488,6 +506,12 @@ const supportedTokens = [ decimals: 18, networks: [{ chain: base, address: '0xb0505e5a99abd03d94a1169e638B78EDfEd26ea4' }], }, + { + symbol: 'MaticX', + img: require('../imgs/tokens/maticx.png') as string, + decimals: 18, + networks: [{ chain: polygon, address: '0xfa68fb4628dff1028cfec22b4162fccd0d45efb6' }], + }, // rewards { symbol: 'WELL', diff --git a/src/utils/types.ts b/src/utils/types.ts index 07ad0012..37b0d312 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,4 +1,5 @@ import { Address } from 'viem'; +import { SupportedNetworks } from './networks'; export type MarketPosition = { state: { @@ -362,6 +363,7 @@ export type UserRebalancerInfo = { transactions: { transactionHash: string; }[]; + network: SupportedNetworks; }; export type AgentMetadata = { diff --git a/src/utils/urls.ts b/src/utils/urls.ts index 94fb6fc6..f06c88eb 100644 --- a/src/utils/urls.ts +++ b/src/utils/urls.ts @@ -1,5 +1,24 @@ +import { SupportedNetworks } from './networks'; + export const URLS = { MORPHO_BLUE_API: 'https://blue-api.morpho.org/graphql', MORPHO_REWARDS_API: 'https://rewards.morpho.org/v1', - MONARCH_AGENT_API: 'https://api.studio.thegraph.com/query/94369/monarch-agent/version/latest', } as const; + +export const MONARCH_AGENT_URLS: Record = { + [SupportedNetworks.Base]: + 'https://api.studio.thegraph.com/query/110397/monarch-agent-base/version/latest', + [SupportedNetworks.Polygon]: + 'https://api.studio.thegraph.com/query/110397/monarch-agent-polygon/version/latest', +} as Record; + +// Helper function to get URL by chainId, returns undefined if not supported +export const getMonarchAgentUrl = (chainId: number): string | undefined => { + if (chainId === SupportedNetworks.Base) { + return MONARCH_AGENT_URLS[SupportedNetworks.Base]; + } + if (chainId === SupportedNetworks.Polygon) { + return MONARCH_AGENT_URLS[SupportedNetworks.Polygon]; + } + return undefined; +};