From 0509a11a7038e0692bf28945067e18839a73dd86 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sun, 27 Apr 2025 13:16:05 +0800 Subject: [PATCH 01/12] feat: polygon --- app/admin/stats/page.tsx | 20 ++++++---- app/api/balances/route.ts | 1 + app/api/block/route.ts | 4 +- app/api/positions/historical/route.ts | 10 ++--- src/components/Avatar/Avatar.tsx | 4 +- src/components/WithdrawModalContent.tsx | 5 ++- src/config/dataSources.ts | 10 ++--- src/contexts/MarketsContext.tsx | 1 + src/hooks/useAuthorizeAgent.ts | 8 ++-- src/hooks/useMorphoBundlerAuthorization.ts | 10 ++--- src/hooks/useSupplyMarket.ts | 2 + src/hooks/useUserBalances.ts | 5 ++- src/imgs/chains/polygon.png | Bin 0 -> 78085 bytes src/imgs/tokens/maticx.png | Bin 0 -> 1757 bytes src/store/createWagmiConfig.ts | 4 +- src/store/supportedChains.ts | 10 ++--- src/utils/external.ts | 6 +++ src/utils/morpho.ts | 43 ++++++++++++++++----- src/utils/networks.ts | 6 +++ src/utils/rpc.ts | 23 ++++++++++- src/utils/subgraph-urls.ts | 6 ++- src/utils/tokens.ts | 24 ++++++++++-- 22 files changed, 148 insertions(+), 54 deletions(-) create mode 100644 src/imgs/chains/polygon.png create mode 100644 src/imgs/tokens/maticx.png diff --git a/app/admin/stats/page.tsx b/app/admin/stats/page.tsx index 8ac7228b..b3ccc2e0 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,7 +58,10 @@ 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); 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/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..fc043bd0 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 Morpho API } }; 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/hooks/useAuthorizeAgent.ts b/src/hooks/useAuthorizeAgent.ts index a776f988..3aa350c4 100644 --- a/src/hooks/useAuthorizeAgent.ts +++ b/src/hooks/useAuthorizeAgent.ts @@ -6,7 +6,7 @@ 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 { MONARCH_TX_IDENTIFIER, getMorphoAddress } from '@/utils/morpho'; import { SupportedNetworks } from '@/utils/networks'; import { Market } from '@/utils/types'; export enum AuthorizeAgentStep { @@ -42,14 +42,14 @@ export const useAuthorizeAgent = ( const { signTypedDataAsync } = useSignTypedData(); const { data: isAuthorized } = useReadContract({ - address: MORPHO, + address: getMorphoAddress(chainId as SupportedNetworks), abi: morphoAbi, functionName: 'isAuthorized', args: [account as Address, AGENT_CONTRACT], }); const { data: nonce } = useReadContract({ - address: MORPHO, + address: getMorphoAddress(chainId as SupportedNetworks), abi: morphoAbi, functionName: 'nonce', args: [account as Address], @@ -95,7 +95,7 @@ export const useAuthorizeAgent = ( if (isAuthorized === false) { const domain = { chainId: SupportedNetworks.Base, - verifyingContract: MORPHO as Address, + verifyingContract: getMorphoAddress(chainId as SupportedNetworks) as Address, }; const types = { 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/imgs/chains/polygon.png b/src/imgs/chains/polygon.png new file mode 100644 index 0000000000000000000000000000000000000000..e4e4a8a005dc8a2798c1cac8f59134b8927439aa GIT binary patch literal 78085 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�P(42nf$%4Reys>}ne?&_eD9j3V0n;xDQvP{*T5 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 SLwSUP{HQ2s%2z=xLjNDFo4m9D literal 0 HcmV?d00001 diff --git a/src/imgs/tokens/maticx.png b/src/imgs/tokens/maticx.png new file mode 100644 index 0000000000000000000000000000000000000000..6a33317f740fe6f4965727f3045450eb6df24ca6 GIT binary patch literal 1757 zcmV<31|s>1P)Px*mq|oHR9HuqmupazRT#&AFU#s;nxs^SiAkd-;r%vAK+`m)m5t5#p&$p%FdWjYK7v+46g3wZO(h!cb8qX_tSaL zYg6Idp3bQ=02Z$4sOG`^hOiX0es#UiOl{PLR zAyZLVi7d;?dM!}hAPWv1G8+m7AWnWzLW&b7Syx($!{K1iAS;cHACQx?G7$J6IKgMG zuBu{nQIP_mrl!UKAwjncbIqPG0~n&An+0XTuxirdS|aFngb784+SYilcIrKPmCwHdB9 zu7SxLsi~=Ks;D3%J>BS4K)e<31W*eUnE{Ba)+yL7V&-tH$7YLB40ydB8n!l2V7Eu+ zKp585)zy)gm#2CH@OZq`)V!sK!v{P9{GofG5m+uJ*4EZiXtxt(u_z1c>+7-G3l&%0 zA2XfQq!nDe_B#hJHXuj1dpc49!fr2694+{;bLUPnGc)z!4}h12iCEy0Imv9^yqSXh zeA?RE$;il17CiTG4JiqWuvv#HL_FVfgzvj*IMcf?yk|6U`}X}rT3UMO=35Os`@jmF5L$Lcz0ER9QW}^tdJ)Gie9f-T*HpJ7F*|-a z3mz$>_vSxrZhf@B7X}EAX=uA>9|mS<$wIs5fhjtGt@{^o>Bg_7^ea)6-odpS7ueph zn1$oZNr;`y;d7sJ^zV;NfCfZzSd3l=hV?swwr)wvHl~bzmH>dj%$f1oO5h%Q=oRAb z8V8buU-ok5N(bL}RdcbgBfME1lSFSAc)?w|2M(OtPRq#+hNBXPW-x!!8pgy<#p82x z{PG@7xW47_3Hdw_JC$4BKECUElV4o#8EYh2%^b!KlGau_LZIknWk&o`79_7D#(FpB zdyewspY;kSl1Dv-CCbX_D+l@ZL=~6azoKvO%!DfDj$6ua7n^B1{;DZ)iDqyOj~oJ% z^Wp}luwYUN58a!FB}z>!|Ms3_-(TDC`ESzMy~~&_3{H!GiRY(Q2LME-ff53Zgty5m zB6W>i%-nI=oV|L8A5Lvz*~~8(dDl3c`)7&pYloAVJ}QgFQv`tBG~cS5;L>QBe^G4jeF2ark@-i9;V_{-k2YjF_P$uUo!8I$eAC>V#UnTKDf~ zUTP|hjg92w|<{+z)GKvulW+FG$a$LTtoZ>f%v0AN4M7>@w4Gmkd z7lfAMff*tyVQyX?R;?ThJ5HyQl9Cc}E}tw1mt%p*3DRmor1J7|ii?Y}Mq3TZA_R%h z3TFH@|8P*Q*w$sU4vAIgseu4x@7y>SlJClWJ1BB)Yv1-ST zkI2pzRZTBdAca#nIh^neMu1TRf&vmuk2`;un2ZaVzEOfg8B~E7 z?i=VbNRlr~mh1c^IKcJ4o@VB)CTNdSp(X=5JpvjPkE}gTsTRsNBKVMZ!9er42Zr}( z&vOZ;$smB~LPk8#{{)U=VK2Ve7Pxjt&-4EPC%JpZctgy=00000NkvXXu0mjfG7L5w literal 0 HcmV?d00001 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/morpho.ts b/src/utils/morpho.ts index 40e80643..84f9f39b 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..20dd5390 100644 --- a/src/utils/networks.ts +++ b/src/utils/networks.ts @@ -1,6 +1,7 @@ enum SupportedNetworks { Mainnet = 1, Base = 8453, + Polygon = 137, } const isSupportedChain = (chainId: number) => { @@ -18,6 +19,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) => { 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/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..27037cb7 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,6 +199,11 @@ 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, }, @@ -252,7 +261,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 +500,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', From ca5706d96442b616deb02c11deb970048dc9a118 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sun, 27 Apr 2025 13:25:21 +0800 Subject: [PATCH 02/12] chore: fix build --- src/services/statsService.ts | 153 +++-------------------------------- 1 file changed, 9 insertions(+), 144 deletions(-) diff --git a/src/services/statsService.ts b/src/services/statsService.ts index ef02bc7a..fa2e30c7 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -42,11 +42,9 @@ export const fetchTransactionsByTimeRange = async ( startTime: number, endTime: number, networkId: SupportedNetworks = SupportedNetworks.Base, - apiEndpoint?: string, + 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( @@ -132,139 +130,6 @@ 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 @@ -272,7 +137,7 @@ export const fetchVolumeOverTime = async ( export const fetchPlatformStats = async ( timeframe: TimeFrame, networkId: SupportedNetworks = SupportedNetworks.Base, - apiEndpoint?: string, + endpoint: string, ): Promise => { try { console.log(`Fetching platform stats for timeframe: ${timeframe} from network ${networkId}`); @@ -284,13 +149,13 @@ export const fetchPlatformStats = async ( currentRange.startTime, currentRange.endTime, networkId, - apiEndpoint, + endpoint, ), fetchTransactionsByTimeRange( previousRange.startTime, previousRange.endTime, networkId, - apiEndpoint, + endpoint, ), ]); @@ -317,7 +182,7 @@ export const fetchPlatformStats = async ( export const fetchAssetMetrics = async ( timeframe: TimeFrame, networkId: SupportedNetworks = SupportedNetworks.Base, - apiEndpoint?: string, + endpoint: string, ): Promise => { try { console.log(`Fetching asset metrics for timeframe: ${timeframe} from network ${networkId}`); @@ -326,7 +191,7 @@ export const fetchAssetMetrics = async ( startTime, endTime, networkId, - apiEndpoint, + endpoint, ); console.log(`Processing ${transactions.length} transactions for asset metrics`); @@ -395,7 +260,7 @@ export const fetchAssetMetrics = async ( export const fetchAllStatistics = async ( timeframe: TimeFrame = '30D', networkId: SupportedNetworks = SupportedNetworks.Base, - apiEndpoint?: string, + endpoint: string, ): Promise<{ platformStats: PlatformStats; assetMetrics: AssetVolumeData[]; @@ -405,8 +270,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(); From 54c8b6e0c2ebd23d3473dd7a6093d5816cce5662 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sun, 27 Apr 2025 13:30:25 +0800 Subject: [PATCH 03/12] fix: build --- app/admin/stats/page.tsx | 2 +- src/services/statsService.ts | 31 ++++++------------------------- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/app/admin/stats/page.tsx b/app/admin/stats/page.tsx index b3ccc2e0..f9c1b9c0 100644 --- a/app/admin/stats/page.tsx +++ b/app/admin/stats/page.tsx @@ -64,7 +64,7 @@ export default function StatsPage() { } 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/src/services/statsService.ts b/src/services/statsService.ts index fa2e30c7..1a4de0ba 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -1,51 +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, + networkId: SupportedNetworks, endpoint: string, ): Promise => { try { - console.log( `Fetching transactions between ${new Date(startTime * 1000).toISOString()} and ${new Date( endTime * 1000, @@ -130,13 +112,12 @@ export const fetchTransactionsByTimeRange = async ( } }; - /** * Fetch and calculate platform-wide statistics */ export const fetchPlatformStats = async ( timeframe: TimeFrame, - networkId: SupportedNetworks = SupportedNetworks.Base, + networkId: SupportedNetworks, endpoint: string, ): Promise => { try { @@ -181,7 +162,7 @@ export const fetchPlatformStats = async ( */ export const fetchAssetMetrics = async ( timeframe: TimeFrame, - networkId: SupportedNetworks = SupportedNetworks.Base, + networkId: SupportedNetworks, endpoint: string, ): Promise => { try { @@ -258,9 +239,9 @@ export const fetchAssetMetrics = async ( * Build a statistics payload with all relevant metrics */ export const fetchAllStatistics = async ( - timeframe: TimeFrame = '30D', - networkId: SupportedNetworks = SupportedNetworks.Base, + networkId: SupportedNetworks, endpoint: string, + timeframe: TimeFrame = '30D', ): Promise<{ platformStats: PlatformStats; assetMetrics: AssetVolumeData[]; From aff9de36316ab8117536aaa2b52d8813b4db19fd Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sun, 27 Apr 2025 13:32:43 +0800 Subject: [PATCH 04/12] chore: fix comment IRM title matching --- src/utils/morpho.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/morpho.ts b/src/utils/morpho.ts index 84f9f39b..ed3ace35 100644 --- a/src/utils/morpho.ts +++ b/src/utils/morpho.ts @@ -42,7 +42,7 @@ export const getIRMTitle = (address: string) => { return 'Adaptive Curve'; case '0x46415998764c29ab2a25cbea6254146d50d22687': // on base return 'Adaptive Curve'; - case '0xe675A2161D4a6E2de2eeD70ac98EEBf257FBF0B0': // on polygon + case '0xe675a2161d4a6e2de2eed70ac98eebf257fbf0b0': // on polygon return 'Adaptive Curve'; default: return 'Unknown IRM'; From 67b7a9c0d56a778fd2ebed80bf4f6345021347b9 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sun, 27 Apr 2025 13:39:03 +0800 Subject: [PATCH 05/12] fix: historical source --- src/config/dataSources.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/config/dataSources.ts b/src/config/dataSources.ts index fc043bd0..23cbe41f 100644 --- a/src/config/dataSources.ts +++ b/src/config/dataSources.ts @@ -10,7 +10,7 @@ export const getMarketDataSource = (network: SupportedNetworks): 'morpho' | 'sub case SupportedNetworks.Base: return 'morpho'; default: - return 'subgraph'; // 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'; } }; From 687084c1b8bb8cc2b242f51a479ef398c8aa4850 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sun, 27 Apr 2025 15:43:30 +0800 Subject: [PATCH 06/12] feat: local whitelist oracles --- src/config/oracle-whitelist.ts | 123 ++++++++++++++++++++++++++++ src/data-sources/subgraph/market.ts | 30 ++++++- src/imgs/tokens/wpol.webp | Bin 0 -> 594 bytes src/utils/tokens.ts | 6 ++ 4 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 src/config/oracle-whitelist.ts create mode 100644 src/imgs/tokens/wpol.webp 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/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/imgs/tokens/wpol.webp b/src/imgs/tokens/wpol.webp new file mode 100644 index 0000000000000000000000000000000000000000..acc75ce49631cac911fb3f2e6e292587a7591ccc GIT binary patch literal 594 zcmV-Y0eT@V`P5>m1+x zW5YotNs5$x_xvvd{cCy)4BHSQNm6W8Jw^x5zmDgL;r}VOv2Cl${8j?(Wgd~BhZz8XkxPc z>$G|o)9U}p3=v}}jh@c%5naTO-gE&Zf-;!L{mj(DCV_munS-x*Esx#n^xy9Pnm=eD z5FkNuB)A%epv6cC>=Z`W3PwUO1VYe>1_*(GW{MY(Kn4yVh)R7^&c)Dzaw?}^I-*1P zB0MQ~eLt_){$ZVR5ld2FOY9_W@d%IOvntsGogu^q1w(Z-P}xJNQJ zPqgtJlCj&){MR?RNHVrBB=69aY3Z)2!hkqDt#aAz5fDda>82JyLL8Yh2XYPpu>k;} zL_r*z*K?dnZKhHS*f+7D=kSLIacD}qtq&LC$PCeS9aM**rx#_6cBMgBT$N6hsSqZB zn(l}+0su6BOX;2AD*S!z|Cf>Hcnll!&u4BKvp0Mq;*Rfa0MTj4PSNh)2N7EVGV?pw gh=&h!K~^za#?zM}r<_$hHx<}J{+gGTA{!^eV1ketQ2+n{ literal 0 HcmV?d00001 diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts index 27037cb7..0f241d6d 100644 --- a/src/utils/tokens.ts +++ b/src/utils/tokens.ts @@ -207,6 +207,12 @@ const supportedTokens = [ ], 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, From 686da53f99452838d01016a5f3655648dc777f45 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sun, 27 Apr 2025 16:47:22 +0800 Subject: [PATCH 07/12] feat: default hiding positions --- .../components/PositionsSummaryTable.tsx | 47 ++++++++++++++++++- .../components/SuppliedMarketsDetail.tsx | 17 +++++-- src/hooks/useUserPositionsSummaryData.ts | 5 -- src/utils/storageKeys.ts | 2 + 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/app/positions/components/PositionsSummaryTable.tsx b/app/positions/components/PositionsSummaryTable.tsx index 96ce0ef8..4ca74c9f 100644 --- a/app/positions/components/PositionsSummaryTable.tsx +++ b/app/positions/components/PositionsSummaryTable.tsx @@ -1,7 +1,16 @@ import React, { useMemo, useState, useEffect } from 'react'; -import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Tooltip } from '@nextui-org/react'; +import { + Dropdown, + DropdownTrigger, + DropdownMenu, + DropdownItem, + Tooltip, + Switch, + Button as NextUIButton, +} from '@nextui-org/react'; import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'; import { ReloadIcon } from '@radix-ui/react-icons'; +import { GearIcon } from '@radix-ui/react-icons'; import { motion, AnimatePresence } from 'framer-motion'; import Image from 'next/image'; import { BsQuestionCircle } from 'react-icons/bs'; @@ -12,6 +21,7 @@ import { useAccount } from 'wagmi'; import { Button } from '@/components/common/Button'; import { TokenIcon } from '@/components/TokenIcon'; import { TooltipContent } from '@/components/TooltipContent'; +import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useStyledToast } from '@/hooks/useStyledToast'; import { formatReadable, formatBalance } from '@/utils/balance'; import { getNetworkImg } from '@/utils/networks'; @@ -21,6 +31,7 @@ import { groupPositionsByLoanAsset, processCollaterals, } from '@/utils/positions'; +import { PositionsShowEmptyKey } from '@/utils/storageKeys'; import { MarketPosition, GroupedPosition, @@ -65,6 +76,10 @@ export function PositionsSummaryTable({ ); const [earningsPeriod, setEarningsPeriod] = useState(EarningsPeriod.Day); + const [showEmptyPositions, setShowEmptyPositions] = useLocalStorage( + PositionsShowEmptyKey, + false, + ); const { address } = useAccount(); const toast = useStyledToast(); @@ -154,6 +169,35 @@ export function PositionsSummaryTable({ Refresh + + + + + + + + +
+ Show Empty Positions + +
+
+
+
@@ -335,6 +379,7 @@ export function PositionsSummaryTable({ setShowWithdrawModal={setShowWithdrawModal} setShowSupplyModal={setShowSupplyModal} setSelectedPosition={setSelectedPosition} + showEmptyPositions={showEmptyPositions} /> diff --git a/app/positions/components/SuppliedMarketsDetail.tsx b/app/positions/components/SuppliedMarketsDetail.tsx index 9804e5e8..c4bdf766 100644 --- a/app/positions/components/SuppliedMarketsDetail.tsx +++ b/app/positions/components/SuppliedMarketsDetail.tsx @@ -14,6 +14,7 @@ type SuppliedMarketsDetailProps = { setShowWithdrawModal: (show: boolean) => void; setShowSupplyModal: (show: boolean) => void; setSelectedPosition: (position: MarketPosition) => void; + showEmptyPositions: boolean; }; function WarningTooltip({ warnings }: { warnings: WarningWithDetail[] }) { @@ -42,14 +43,24 @@ export function SuppliedMarketsDetail({ setShowWithdrawModal, setShowSupplyModal, setSelectedPosition, + showEmptyPositions, }: 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[]) => { @@ -121,7 +132,7 @@ export function SuppliedMarketsDetail({ - {sortedActiveMarkets.map((position) => { + {filteredMarkets.map((position) => { const suppliedAmount = Number( formatBalance(position.state.supplyAssets, position.market.loanAsset.decimals), ); diff --git a/src/hooks/useUserPositionsSummaryData.ts b/src/hooks/useUserPositionsSummaryData.ts index 2a8eaac0..d59373e1 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]); @@ -198,10 +197,6 @@ const useUserPositionsSummaryData = (user: string | undefined) => { // 3. We have positions but no earnings data yet const isPositionsLoading = !hasInitialData || positionsLoading || !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/utils/storageKeys.ts b/src/utils/storageKeys.ts index 81164e7c..1c8d7acd 100644 --- a/src/utils/storageKeys.ts +++ b/src/utils/storageKeys.ts @@ -7,6 +7,8 @@ export const MarketEntriesPerPageKey = 'monarch_marketsEntriesPerPage'; export const MarketsShowUnknownKey = 'monarch_marketsShowUnknown'; export const MarketsShowUnknownOracleKey = 'monarch_marketsShowUnknownOracle'; +export const PositionsShowEmptyKey = 'monarch_positionsShowEmpty'; + export const ThemeKey = 'theme'; export const CacheMarketPositionKeys = 'monarch_cache_market_unique_keys'; From 9a03de72705c62147fe95fc804bc067b4e1b031b Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 28 Apr 2025 14:16:41 +0800 Subject: [PATCH 08/12] chore: show collateral exposure --- app/positions/components/PositionsContent.tsx | 7 +- .../components/PositionsSummaryTable.tsx | 27 +++++++- .../components/SuppliedMarketsDetail.tsx | 67 ++++++++++--------- app/positions/components/agent/SetupAgent.tsx | 5 ++ .../components/agent/SetupAgentModal.tsx | 4 +- src/hooks/useAuthorizeAgent.ts | 29 ++++---- src/utils/monarch-agent.ts | 13 +++- src/utils/storageKeys.ts | 3 +- 8 files changed, 104 insertions(+), 51 deletions(-) diff --git a/app/positions/components/PositionsContent.tsx b/app/positions/components/PositionsContent.tsx index 969330ac..8546994c 100644 --- a/app/positions/components/PositionsContent.tsx +++ b/app/positions/components/PositionsContent.tsx @@ -51,9 +51,10 @@ 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 && + (position.market.morphoBlue.chain.id === SupportedNetworks.Base || + position.market.morphoBlue.chain.id === SupportedNetworks.Polygon) && BigInt(position.state.supplyShares) > 0 ); }); @@ -84,7 +85,7 @@ export default function Positions() { Report - {isOwner && hasActivePositionOnBase && ( + {isOwner && hasActivePositionForAgent && (
diff --git a/app/positions/components/agent/SetupAgent.tsx b/app/positions/components/agent/SetupAgent.tsx index 93ef93b7..4ce3e10e 100644 --- a/app/positions/components/agent/SetupAgent.tsx +++ b/app/positions/components/agent/SetupAgent.tsx @@ -13,6 +13,7 @@ import { SupportedNetworks } from '@/utils/networks'; import { Market, MarketPosition, UserRebalancerInfo } from '@/utils/types'; type MarketGroup = { + network: SupportedNetworks; loanAsset: { address: string; symbol: string; @@ -78,6 +79,8 @@ export function SetupAgent({ const [showAllMarkets, setShowAllMarkets] = useState(false); const [showProcessModal, setShowProcessModal] = useState(false); + const [targetNetwork, setTargetNetwork] = useState(SupportedNetworks.Base); + const isInPending = (market: Market) => pendingCaps.some((cap) => cap.market.uniqueKey === market.uniqueKey && cap.amount > 0); @@ -107,6 +110,7 @@ export function SetupAgent({ if (!groups[loanAssetKey]) { groups[loanAssetKey] = { + network: market.morphoBlue.chain.id, loanAsset: market.loanAsset, authorizedMarkets: [], activeMarkets: [], @@ -179,6 +183,7 @@ export function SetupAgent({ const { executeBatchSetupAgent, currentStep } = useAuthorizeAgent( KnownAgents.MAX_APY, pendingCaps, + targetNetwork, onNext, ); diff --git a/app/positions/components/agent/SetupAgentModal.tsx b/app/positions/components/agent/SetupAgentModal.tsx index 049e5445..f988a675 100644 --- a/app/positions/components/agent/SetupAgentModal.tsx +++ b/app/positions/components/agent/SetupAgentModal.tsx @@ -197,7 +197,9 @@ export function SetupAgentModal({ m.morphoBlue.chain.id === SupportedNetworks.Base, + (m) => + m.morphoBlue.chain.id === SupportedNetworks.Base || + m.morphoBlue.chain.id === SupportedNetworks.Polygon, )} pendingCaps={pendingCaps} addToPendingCaps={addToPendingCaps} diff --git a/src/hooks/useAuthorizeAgent.ts b/src/hooks/useAuthorizeAgent.ts index 3aa350c4..90ef8de8 100644 --- a/src/hooks/useAuthorizeAgent.ts +++ b/src/hooks/useAuthorizeAgent.ts @@ -5,7 +5,7 @@ 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 { getAgentContract } from '@/utils/monarch-agent'; import { MONARCH_TX_IDENTIFIER, getMorphoAddress } from '@/utils/morpho'; import { SupportedNetworks } from '@/utils/networks'; import { Market } from '@/utils/types'; @@ -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,24 @@ export const useAuthorizeAgent = ( const { switchChainAsync } = useSwitchChain(); const { address: account, chainId } = useAccount(); + const { signTypedDataAsync } = useSignTypedData(); + const AGENT_CONTRACT = getAgentContract(targetChainId); + const { data: isAuthorized } = useReadContract({ - address: getMorphoAddress(chainId as SupportedNetworks), + address: getMorphoAddress(targetChainId), abi: morphoAbi, functionName: 'isAuthorized', args: [account as Address, AGENT_CONTRACT], }); const { data: nonce } = useReadContract({ - address: getMorphoAddress(chainId as SupportedNetworks), + address: getMorphoAddress(targetChainId), abi: morphoAbi, functionName: 'nonce', args: [account as Address], - chainId: SupportedNetworks.Base, + chainId: targetChainId, }); const { data: rebalancerAddress } = useReadContract({ @@ -61,7 +65,7 @@ export const useAuthorizeAgent = ( abi: monarchAgentAbi, functionName: 'rebalancers', args: [account as Address], - chainId: SupportedNetworks.Base, + chainId: targetChainId, query: { enabled: !!account }, }); @@ -70,7 +74,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 +84,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 +98,8 @@ export const useAuthorizeAgent = ( setCurrentStep(AuthorizeAgentStep.Authorize); if (isAuthorized === false) { const domain = { - chainId: SupportedNetworks.Base, - verifyingContract: getMorphoAddress(chainId as SupportedNetworks) as Address, + chainId: targetChainId, + verifyingContract: getMorphoAddress(targetChainId) as Address, }; const types = { @@ -195,7 +199,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 +224,7 @@ export const useAuthorizeAgent = ( rebalancerAddress, chainId, switchChainAsync, + targetChainId, ], ); diff --git a/src/utils/monarch-agent.ts b/src/utils/monarch-agent.ts index e4ddc17c..c88cb6e6 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/storageKeys.ts b/src/utils/storageKeys.ts index 1c8d7acd..e73cb097 100644 --- a/src/utils/storageKeys.ts +++ b/src/utils/storageKeys.ts @@ -7,7 +7,8 @@ export const MarketEntriesPerPageKey = 'monarch_marketsEntriesPerPage'; export const MarketsShowUnknownKey = 'monarch_marketsShowUnknown'; export const MarketsShowUnknownOracleKey = 'monarch_marketsShowUnknownOracle'; -export const PositionsShowEmptyKey = 'monarch_positionsShowEmpty'; +export const PositionsShowEmptyKey = 'positions:show-empty'; +export const PositionsShowCollateralExposureKey = 'positions:show-collateral-exposure'; export const ThemeKey = 'theme'; From fee535dec21c2a29d4a8edffad725adda6cab9e3 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 28 Apr 2025 22:26:27 +0800 Subject: [PATCH 09/12] feat: set monarch agent on polygon --- app/positions/components/PositionsContent.tsx | 5 +- app/positions/components/agent/Main.tsx | 17 +- app/positions/components/agent/SetupAgent.tsx | 152 +++++++++++++++--- .../components/agent/SetupAgentModal.tsx | 10 +- src/hooks/useAuthorizeAgent.ts | 3 +- src/hooks/useUserRebalancerInfo.ts | 7 +- src/utils/monarch-agent.ts | 2 +- src/utils/networks.ts | 16 +- src/utils/types.ts | 2 + 9 files changed, 163 insertions(+), 51 deletions(-) diff --git a/app/positions/components/PositionsContent.tsx b/app/positions/components/PositionsContent.tsx index 8546994c..49071317 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'; @@ -53,8 +53,7 @@ export default function Positions() { const hasActivePositionForAgent = marketPositions?.some((position) => { return ( - (position.market.morphoBlue.chain.id === SupportedNetworks.Base || - position.market.morphoBlue.chain.id === SupportedNetworks.Polygon) && + isAgentAvailable(position.market.morphoBlue.chain.id) && BigInt(position.state.supplyShares) > 0 ); }); diff --git a/app/positions/components/agent/Main.tsx b/app/positions/components/agent/Main.tsx index 947175dc..f9261d05 100644 --- a/app/positions/components/agent/Main.tsx +++ b/app/positions/components/agent/Main.tsx @@ -13,12 +13,11 @@ import { UserRebalancerInfo } from '@/utils/types'; const img = require('../../../../src/imgs/agent/agent-detailed.png') as string; type MainProps = { - account?: string; onNext: () => void; userRebalancerInfo: UserRebalancerInfo; }; -export function Main({ account, onNext, userRebalancerInfo }: MainProps) { +export function Main({ onNext, userRebalancerInfo }: MainProps) { const agent = findAgent(userRebalancerInfo.rebalancer); const { markets } = useMarkets(); @@ -126,20 +125,6 @@ export function Main({ account, onNext, userRebalancerInfo }: MainProps) { )} - -
-

Automations

-
- - View All ({userRebalancerInfo.transactions?.length ?? 0}) - -
-
diff --git a/app/positions/components/agent/SetupAgent.tsx b/app/positions/components/agent/SetupAgent.tsx index 4ce3e10e..995fd4de 100644 --- a/app/positions/components/agent/SetupAgent.tsx +++ b/app/positions/components/agent/SetupAgent.tsx @@ -1,15 +1,30 @@ import { useState, useMemo, useCallback, useEffect } from 'react'; -import { Checkbox } from '@nextui-org/react'; +import { + Checkbox, + Dropdown, + DropdownTrigger, + DropdownMenu, + DropdownItem, + Tooltip, +} from '@nextui-org/react'; import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'; import { motion, AnimatePresence } from 'framer-motion'; +import Image from 'next/image'; +import { BsQuestionCircle } from 'react-icons/bs'; import { formatUnits, maxUint256 } from 'viem'; import { AgentSetupProcessModal } from '@/components/AgentSetupProcessModal'; import { Button } from '@/components/common/Button'; import { MarketInfoBlockCompact } from '@/components/common/MarketInfoBlock'; import { TokenIcon } from '@/components/TokenIcon'; +import { TooltipContent } from '@/components/TooltipContent'; import { MarketCap, useAuthorizeAgent } from '@/hooks/useAuthorizeAgent'; import { findAgent, KnownAgents } from '@/utils/monarch-agent'; -import { SupportedNetworks } from '@/utils/networks'; +import { + getNetworkName, + getNetworkImg, + SupportedNetworks, + isAgentAvailable, +} from '@/utils/networks'; import { Market, MarketPosition, UserRebalancerInfo } from '@/utils/types'; type MarketGroup = { @@ -43,20 +58,27 @@ function MarketRow({ market, isSelected, onToggle, + isDisabled, }: { market: Market; isSelected: boolean; onToggle: (selected: boolean) => void; + isDisabled: boolean; }) { return ( -
+
!isDisabled && onToggle(selected)} size="sm" color="primary" className="mr-0" + isDisabled={isDisabled} />
@@ -81,6 +103,17 @@ export function SetupAgent({ const [targetNetwork, setTargetNetwork] = useState(SupportedNetworks.Base); + // --- Network Logic Start --- + const availableNetworks = useMemo(() => { + const networkSet = new Set(); + positions.forEach((p) => { + if (isAgentAvailable(p.market.morphoBlue.chain.id)) { + networkSet.add(p.market.morphoBlue.chain.id); + } + }); + return Array.from(networkSet).sort(); + }, [positions, allMarkets]); + const isInPending = (market: Market) => pendingCaps.some((cap) => cap.market.uniqueKey === market.uniqueKey && cap.amount > 0); @@ -143,20 +176,27 @@ export function SetupAgent({ } }); - return Object.values(groups); + // Sort groups by network ID first, then potentially by loan asset symbol + return Object.values(groups).sort((a, b) => { + if (a.network !== b.network) { + return a.network - b.network; + } + return a.loanAsset.symbol.localeCompare(b.loanAsset.symbol); + }); }, [allMarkets, positions, userRebalancerInfo]); - // Pre-select active markets only once when component mounts + // Pre-select active markets only once when component mounts and target network is set useEffect(() => { let mounted = true; - if (!hasPreselected && groupedMarkets.length > 0) { + if (!hasPreselected && groupedMarkets.length > 0 && targetNetwork) { groupedMarkets.forEach((group) => { - // pre-select active markets but not already authorized - group.activeMarkets.forEach((market) => { - if (!isInPending(market)) { - addToPendingCaps(market, maxUint256); - } - }); + if (group.network === targetNetwork) { + group.activeMarkets.forEach((market) => { + if (!isInPending(market)) { + addToPendingCaps(market, maxUint256); + } + }); + } }); if (mounted) { setHasPreselected(true); @@ -166,7 +206,7 @@ export function SetupAgent({ return () => { mounted = false; }; - }, [hasPreselected, groupedMarkets, isInPending, addToPendingCaps]); + }, [hasPreselected, groupedMarkets, isInPending, addToPendingCaps, targetNetwork]); const toggleGroup = (key: string) => { setExpandedGroups((prev) => @@ -201,7 +241,47 @@ export function SetupAgent({
)}
- The agent can only reallocate funds among your approved markets. + The agent can only reallocate funds among approved markets. + {availableNetworks.length > 1 && targetNetwork && ( + + + + + { + const selectedKey = Array.from(keys)[0]; + setTargetNetwork(Number(selectedKey) as SupportedNetworks); + setHasPreselected(false); + }} + > + {availableNetworks.map((networkId) => ( + +
+ + {getNetworkName(networkId) ?? `Network ${networkId}`} +
+
+ ))} +
+
+ )}
{groupedMarkets.map((group) => { - const groupKey = group.loanAsset.address; + const groupKey = `${group.loanAsset.address}-${group.network}`; const isExpanded = expandedGroups.includes(groupKey); + const isGroupDisabled = group.network !== targetNetwork; const numMarketsToAdd = [ ...group.activeMarkets, @@ -233,15 +314,19 @@ export function SetupAgent({ onClose={() => setShowProcessModal(false)} /> )} + @@ -290,6 +395,7 @@ export function SetupAgent({ ? removeFromPendingCaps(market) : addToPendingCaps(market, BigInt(0)) } + isDisabled={isGroupDisabled} /> ))}
@@ -309,6 +415,7 @@ export function SetupAgent({ ? addToPendingCaps(market, maxUint256) : removeFromPendingCaps(market) } + isDisabled={isGroupDisabled} /> ))}
@@ -328,6 +435,7 @@ export function SetupAgent({ ? addToPendingCaps(market, maxUint256) : removeFromPendingCaps(market) } + isDisabled={isGroupDisabled} /> ))} @@ -340,6 +448,7 @@ export function SetupAgent({ size="sm" onClick={() => setShowAllMarkets(true)} className="w-full" + isDisabled={isGroupDisabled} > Show More Markets @@ -358,6 +467,7 @@ export function SetupAgent({ ? addToPendingCaps(market, maxUint256) : removeFromPendingCaps(market) } + isDisabled={isGroupDisabled} /> ))} @@ -380,7 +490,7 @@ export function SetupAgent({ variant="solid" color="primary" onPress={handleExecute} - isDisabled={pendingCaps.length === 0} + isDisabled={pendingCaps.length === 0 || !targetNetwork} > Execute diff --git a/app/positions/components/agent/SetupAgentModal.tsx b/app/positions/components/agent/SetupAgentModal.tsx index f988a675..56aeafa8 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'; @@ -188,19 +188,15 @@ export function SetupAgentModal({ )} {currentStep === SetupStep.Main && hasSetupAgent && ( )} {currentStep === SetupStep.Setup && ( - m.morphoBlue.chain.id === SupportedNetworks.Base || - m.morphoBlue.chain.id === SupportedNetworks.Polygon, - )} + allMarkets={allMarkets.filter((m) => isAgentAvailable(m.morphoBlue.chain.id))} pendingCaps={pendingCaps} addToPendingCaps={addToPendingCaps} removeFromPendingCaps={removeFromCaps} diff --git a/src/hooks/useAuthorizeAgent.ts b/src/hooks/useAuthorizeAgent.ts index 90ef8de8..e5ca57e9 100644 --- a/src/hooks/useAuthorizeAgent.ts +++ b/src/hooks/useAuthorizeAgent.ts @@ -40,7 +40,7 @@ export const useAuthorizeAgent = ( const { switchChainAsync } = useSwitchChain(); const { address: account, chainId } = useAccount(); - + const { signTypedDataAsync } = useSignTypedData(); const AGENT_CONTRACT = getAgentContract(targetChainId); @@ -49,6 +49,7 @@ export const useAuthorizeAgent = ( address: getMorphoAddress(targetChainId), abi: morphoAbi, functionName: 'isAuthorized', + chainId: targetChainId, args: [account as Address, AGENT_CONTRACT], }); diff --git a/src/hooks/useUserRebalancerInfo.ts b/src/hooks/useUserRebalancerInfo.ts index 2de737ae..974f9f2e 100644 --- a/src/hooks/useUserRebalancerInfo.ts +++ b/src/hooks/useUserRebalancerInfo.ts @@ -1,5 +1,6 @@ import { useState, useEffect, useCallback } from 'react'; import { userRebalancerInfoQuery } from '@/graphql/morpho-api-queries'; +import { SupportedNetworks } from '@/utils/networks'; import { UserRebalancerInfo } from '@/utils/types'; import { URLS } from '@/utils/urls'; @@ -30,7 +31,11 @@ export function useUserRebalancerInfo(account: string | undefined) { const json = (await response.json()) as { data?: { user?: UserRebalancerInfo } }; if (json.data?.user) { - setData(json.data.user); + const finalData = { + ...json.data.user, + network: SupportedNetworks.Base, + }; + setData(finalData); } setError(null); } catch (err) { diff --git a/src/utils/monarch-agent.ts b/src/utils/monarch-agent.ts index c88cb6e6..446d059d 100644 --- a/src/utils/monarch-agent.ts +++ b/src/utils/monarch-agent.ts @@ -9,7 +9,7 @@ export const getAgentContract = (chain: SupportedNetworks) => { case SupportedNetworks.Polygon: return '0x01c90eEb82f982301fE4bd11e36A5704673CF18C'; default: - return zeroAddress + return zeroAddress; } }; diff --git a/src/utils/networks.ts b/src/utils/networks.ts index 20dd5390..ac4af2af 100644 --- a/src/utils/networks.ts +++ b/src/utils/networks.ts @@ -8,6 +8,12 @@ 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, @@ -36,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/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 = { From 5648f6e4f95b2df8cf6e10ed8fc3485dd2ad13a8 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Tue, 29 Apr 2025 15:59:26 +0800 Subject: [PATCH 10/12] feat: setup agent on polygon --- app/history/components/HistoryContent.tsx | 4 +- app/history/components/HistoryTable.tsx | 11 +- app/positions/components/PositionsContent.tsx | 6 +- .../components/PositionsSummaryTable.tsx | 8 +- app/positions/components/agent/Main.tsx | 217 ++++++++++-------- app/positions/components/agent/SetupAgent.tsx | 172 +++++++------- .../components/agent/SetupAgentModal.tsx | 18 +- src/hooks/useUserRebalancerInfo.ts | 59 +++-- src/utils/positions.ts | 105 +++++---- src/utils/urls.ts | 21 +- 10 files changed, 367 insertions(+), 254 deletions(-) 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 49071317..39616f1e 100644 --- a/app/positions/components/PositionsContent.tsx +++ b/app/positions/components/PositionsContent.tsx @@ -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; @@ -146,7 +146,7 @@ export default function Positions() { setShowSetupAgentModal(false); }} account={account as Address} - userRebalancerInfo={rebalancerInfo} + userRebalancerInfos={rebalancerInfos} /> {isPositionsLoading ? ( @@ -179,7 +179,7 @@ export default function Positions() { refetch={() => void refetch()} isRefetching={isRefetching} isLoadingEarnings={isEarningsLoading} - rebalancerInfo={rebalancerInfo} + rebalancerInfos={rebalancerInfos} /> )} diff --git a/app/positions/components/PositionsSummaryTable.tsx b/app/positions/components/PositionsSummaryTable.tsx index 0f21429a..c3e92427 100644 --- a/app/positions/components/PositionsSummaryTable.tsx +++ b/app/positions/components/PositionsSummaryTable.tsx @@ -55,7 +55,7 @@ type PositionsSummaryTableProps = { refetch: (onSuccess?: () => void) => void; isRefetching: boolean; isLoadingEarnings?: boolean; - rebalancerInfo: UserRebalancerInfo | undefined; + rebalancerInfos: UserRebalancerInfo[]; }; export function PositionsSummaryTable({ @@ -67,7 +67,7 @@ export function PositionsSummaryTable({ isRefetching, isLoadingEarnings, account, - rebalancerInfo, + rebalancerInfos, }: PositionsSummaryTableProps) { const [expandedRows, setExpandedRows] = useState>(new Set()); const [showRebalanceModal, setShowRebalanceModal] = useState(false); @@ -101,8 +101,8 @@ export function PositionsSummaryTable({ }; const groupedPositions = useMemo( - () => groupPositionsByLoanAsset(marketPositions, rebalancerInfo), - [marketPositions, rebalancerInfo], + () => groupPositionsByLoanAsset(marketPositions, rebalancerInfos), + [marketPositions, rebalancerInfos], ); const processedPositions = useMemo( diff --git a/app/positions/components/agent/Main.tsx b/app/positions/components/agent/Main.tsx index f9261d05..4bb1b91a 100644 --- a/app/positions/components/agent/Main.tsx +++ b/app/positions/components/agent/Main.tsx @@ -7,54 +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 = { onNext: () => void; - userRebalancerInfo: UserRebalancerInfo; + userRebalancerInfos: UserRebalancerInfo[]; }; -export function Main({ 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 } + >, + ); + + const explorerUrl = getExplorerURL(agent.address, info.network); + + return ( +
+
+
+

{agent.name}

+ + {networkName} + + + {agent.address.slice(0, 6) + '...' + agent.address.slice(-4)} + +
+ } + title="Agent Active" + detail={`Agent is active on ${networkName}`} + /> + } + > +
+
+ Active +
+
-
-
-
-
-

Strategy

-

{agent.strategyDescription}

-
+
+
+

Strategy

+

{agent.strategyDescription}

+
-
-

Monitoring Positions

-
- {Object.values(loanAssetGroups).map( - ({ address, chainId, markets: marketsForLoanAsset, symbol }) => { - return ( -
- - - {symbol ?? 'Unknown'} ({marketsForLoanAsset.length}) - -
- ); - }, - )} +
+

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

+ )} +
+
-
-
+ ); + })} @@ -381,7 +400,6 @@ export function SetupAgent({ className="overflow-hidden" >
- {/* Authorized markets Markets */} {group.authorizedMarkets.length > 0 && (

Authorized

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

Active Markets

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

Previously Used

@@ -435,20 +451,18 @@ export function SetupAgent({ ? addToPendingCaps(market, maxUint256) : removeFromPendingCaps(market) } - isDisabled={isGroupDisabled} + isDisabled={false} /> ))}
)} - {/* Other Markets */} {group.otherMarkets.length > 0 && !showAllMarkets && ( @@ -467,7 +481,7 @@ export function SetupAgent({ ? addToPendingCaps(market, maxUint256) : removeFromPendingCaps(market) } - isDisabled={isGroupDisabled} + isDisabled={false} /> ))}
@@ -481,7 +495,6 @@ export function SetupAgent({ })}
- {/* Footer */}
diff --git a/app/positions/components/agent/SetupAgentModal.tsx b/app/positions/components/agent/SetupAgentModal.tsx index 56aeafa8..e3e1abd5 100644 --- a/app/positions/components/agent/SetupAgentModal.tsx +++ b/app/positions/components/agent/SetupAgentModal.tsx @@ -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 && ( 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/hooks/useUserRebalancerInfo.ts b/src/hooks/useUserRebalancerInfo.ts index 974f9f2e..8036c618 100644 --- a/src/hooks/useUserRebalancerInfo.ts +++ b/src/hooks/useUserRebalancerInfo.ts @@ -1,46 +1,61 @@ import { useState, useEffect, useCallback } from 'react'; import { userRebalancerInfoQuery } from '@/graphql/morpho-api-queries'; -import { SupportedNetworks } from '@/utils/networks'; +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) { - const finalData = { - ...json.data.user, - network: SupportedNetworks.Base, - }; - setData(finalData); - } - setError(null); + setData(validResults); } catch (err) { console.error('Error fetching rebalancer info:', err); setError(err); + setData([]); } finally { setLoading(false); } @@ -51,7 +66,7 @@ export function useUserRebalancerInfo(account: string | undefined) { }, [fetchData]); return { - rebalancerInfo: data, + rebalancerInfos: data, loading, error, refetch: fetchData, 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/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; +}; From 9b5f867e9f2965be59365e7143c3229b22dbb0c3 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Tue, 29 Apr 2025 16:07:38 +0800 Subject: [PATCH 11/12] chore: fix apy calculation --- app/positions/components/PositionsSummaryTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/positions/components/PositionsSummaryTable.tsx b/app/positions/components/PositionsSummaryTable.tsx index c3e92427..f579623c 100644 --- a/app/positions/components/PositionsSummaryTable.tsx +++ b/app/positions/components/PositionsSummaryTable.tsx @@ -257,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); From 7c8b278fda5655557a3e4386525d9ff95f2123d9 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Thu, 1 May 2025 10:43:05 +0800 Subject: [PATCH 12/12] chore: lint --- src/hooks/useUserPositionsSummaryData.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hooks/useUserPositionsSummaryData.ts b/src/hooks/useUserPositionsSummaryData.ts index d59373e1..238408d3 100644 --- a/src/hooks/useUserPositionsSummaryData.ts +++ b/src/hooks/useUserPositionsSummaryData.ts @@ -195,7 +195,8 @@ 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); const isEarningsLoading = isLoadingBlockNums || isLoadingEarningsQuery || isFetchingEarnings;