diff --git a/.env.local.example b/.env.local.example index e9fbbc1f..d2aff982 100644 --- a/.env.local.example +++ b/.env.local.example @@ -41,6 +41,7 @@ NEXT_PUBLIC_BASE_RPC= NEXT_PUBLIC_POLYGON_RPC= NEXT_PUBLIC_UNICHAIN_RPC= NEXT_PUBLIC_ARBITRUM_RPC= +NEXT_PUBLIC_ETHERLINK_RPC= NEXT_PUBLIC_HYPEREVM_RPC= NEXT_PUBLIC_MONAD_RPC= # Set to "false" locally to skip RPC historical-rate fallback when Morpho API rolling rates fail. diff --git a/src/config/appkit.ts b/src/config/appkit.ts index 382e551c..1c697ddf 100644 --- a/src/config/appkit.ts +++ b/src/config/appkit.ts @@ -5,6 +5,7 @@ import { createStorage, type Storage } from 'wagmi'; import localStorage from 'local-storage-fallback'; import { createAppKit } from '@reown/appkit/react'; import type { AppKitNetwork } from '@reown/appkit/networks'; +import { etherlink } from 'viem/chains'; import { arbitrum, base, mainnet, monad, optimism, polygon, unichain } from 'wagmi/chains'; import { SupportedNetworks, getDefaultRPC, hyperEvm } from '@/utils/networks'; @@ -51,6 +52,7 @@ const customBase = withAppKitRpc(base, getDefaultRPC(SupportedNetworks.Base)); const customPolygon = withAppKitRpc(polygon, getDefaultRPC(SupportedNetworks.Polygon)); const customArbitrum = withAppKitRpc(arbitrum, getDefaultRPC(SupportedNetworks.Arbitrum)); const customUnichain = withAppKitRpc(unichain, getDefaultRPC(SupportedNetworks.Unichain)); +const customEtherlink = withAppKitRpc(etherlink, getDefaultRPC(SupportedNetworks.Etherlink)); const customMonad = withAppKitRpc(monad, getDefaultRPC(SupportedNetworks.Monad)); const customHyperEvm = withAppKitRpc(hyperEvm, getDefaultRPC(SupportedNetworks.HyperEVM)); @@ -62,6 +64,7 @@ export const networks = [ customPolygon, customArbitrum, customUnichain, + customEtherlink, customHyperEvm, customMonad, ] as [AppKitNetwork, ...AppKitNetwork[]]; diff --git a/src/constants/public-allocator.ts b/src/constants/public-allocator.ts index 41f7a6c9..02f87411 100644 --- a/src/constants/public-allocator.ts +++ b/src/constants/public-allocator.ts @@ -7,6 +7,7 @@ export const PUBLIC_ALLOCATOR_ADDRESSES: Partial = { 137: 3, // Polygon 130: 4, // Unichain 42161: 6, // Arbitrum + 42793: 7, // Etherlink 999: 5, // HyperEVM 143: 2, // Monad }; diff --git a/src/features/autovault/components/vault-identity.tsx b/src/features/autovault/components/vault-identity.tsx index d2ee4628..cbed0940 100644 --- a/src/features/autovault/components/vault-identity.tsx +++ b/src/features/autovault/components/vault-identity.tsx @@ -7,7 +7,7 @@ import { ExternalLinkIcon } from '@radix-ui/react-icons'; import { TokenIcon } from '@/components/shared/token-icon'; import { TooltipContent } from '@/components/shared/tooltip-content'; import type { VaultCurator } from '@/constants/vaults/known_vaults'; -import { getVaultURL } from '@/utils/external'; +import { getVaultURL, supportsMorphoAppLinks } from '@/utils/external'; import { VaultIcon } from './vault-icon'; type VaultIdentityVariant = 'chip' | 'inline' | 'icon'; @@ -44,6 +44,7 @@ export function VaultIdentity({ showAddressInTooltip = true, }: VaultIdentityProps) { const vaultHref = useMemo(() => getVaultURL(address, chainId), [address, chainId]); + const canLinkToMorpho = useMemo(() => supportsMorphoAppLinks(chainId), [chainId]); const formattedAddress = `${address.slice(0, 6)}...${address.slice(-4)}`; const displayName = vaultName ?? formattedAddress; const curatorLabel = curator === 'unknown' ? 'Curator unknown' : `Curated by ${curator}`; @@ -92,19 +93,20 @@ export function VaultIdentity({ ); })(); - const interactiveContent = showLink ? ( - e.stopPropagation()} - > - {baseContent} - - ) : ( - baseContent - ); + const interactiveContent = + showLink && canLinkToMorpho ? ( + e.stopPropagation()} + > + {baseContent} + + ) : ( + baseContent + ); if (!showTooltip) { return interactiveContent; diff --git a/src/features/market-detail/components/market-header.tsx b/src/features/market-detail/components/market-header.tsx index 870b608e..8de8004f 100644 --- a/src/features/market-detail/components/market-header.tsx +++ b/src/features/market-detail/components/market-header.tsx @@ -34,7 +34,7 @@ import { convertApyToApr } from '@/utils/rateMath'; import { formatReadable } from '@/utils/balance'; import { getIRMTitle } from '@/utils/morpho'; import { getNetworkImg, getNetworkName, type SupportedNetworks } from '@/utils/networks'; -import { getMarketURL } from '@/utils/external'; +import { getMarketURL, supportsMorphoAppLinks } from '@/utils/external'; import type { Market, MarketPosition, WarningWithDetail } from '@/utils/types'; import { WarningCategory } from '@/utils/types'; import { getRiskLevel, countWarningsByLevel, type RiskLevel } from '@/utils/warnings'; @@ -630,12 +630,14 @@ export function MarketHeader({ Accrue Interest )} - window.open(getMarketURL(resolvedMarketId, network), '_blank')} - startContent={} - > - View on Morpho - + {supportsMorphoAppLinks(network) && ( + window.open(getMarketURL(resolvedMarketId, network), '_blank')} + startContent={} + > + View on Morpho + + )} diff --git a/src/imgs/chains/etherlink.svg b/src/imgs/chains/etherlink.svg new file mode 100644 index 00000000..29bb8525 --- /dev/null +++ b/src/imgs/chains/etherlink.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/imgs/tokens/mbasis.png b/src/imgs/tokens/mbasis.png new file mode 100644 index 00000000..e27854ef Binary files /dev/null and b/src/imgs/tokens/mbasis.png differ diff --git a/src/imgs/tokens/mmev.svg b/src/imgs/tokens/mmev.svg new file mode 100644 index 00000000..9793e4b5 --- /dev/null +++ b/src/imgs/tokens/mmev.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/imgs/tokens/mtbill.png b/src/imgs/tokens/mtbill.png new file mode 100644 index 00000000..c7795106 Binary files /dev/null and b/src/imgs/tokens/mtbill.png differ diff --git a/src/imgs/tokens/xu3o8.png b/src/imgs/tokens/xu3o8.png new file mode 100644 index 00000000..3b679c68 Binary files /dev/null and b/src/imgs/tokens/xu3o8.png differ diff --git a/src/store/createWagmiConfig.ts b/src/store/createWagmiConfig.ts index b2948350..578a78e6 100644 --- a/src/store/createWagmiConfig.ts +++ b/src/store/createWagmiConfig.ts @@ -1,4 +1,5 @@ import { createConfig, http } from 'wagmi'; +import { etherlink } from 'viem/chains'; import { arbitrum, base, mainnet, monad, optimism, polygon, unichain } from 'wagmi/chains'; import type { CustomRpcUrls } from '@/stores/useCustomRpc'; import { SupportedNetworks, getDefaultRPC, hyperEvm } from '@/utils/networks'; @@ -15,12 +16,13 @@ export function createWagmiConfig(customRpcUrls: CustomRpcUrls = {}) { const rpcPolygon = customRpcUrls[SupportedNetworks.Polygon] ?? getDefaultRPC(SupportedNetworks.Polygon); const rpcUnichain = customRpcUrls[SupportedNetworks.Unichain] ?? getDefaultRPC(SupportedNetworks.Unichain); const rpcArbitrum = customRpcUrls[SupportedNetworks.Arbitrum] ?? getDefaultRPC(SupportedNetworks.Arbitrum); + const rpcEtherlink = customRpcUrls[SupportedNetworks.Etherlink] ?? getDefaultRPC(SupportedNetworks.Etherlink); const rpcHyperEVM = customRpcUrls[SupportedNetworks.HyperEVM] ?? getDefaultRPC(SupportedNetworks.HyperEVM); const rpcMonad = customRpcUrls[SupportedNetworks.Monad] ?? getDefaultRPC(SupportedNetworks.Monad); return createConfig({ ssr: true, - chains: [mainnet, optimism, base, polygon, unichain, arbitrum, hyperEvm, monad], + chains: [mainnet, optimism, base, polygon, unichain, arbitrum, etherlink, hyperEvm, monad], transports: { [mainnet.id]: http(rpcMainnet), [optimism.id]: http(rpcOptimism), @@ -28,6 +30,7 @@ export function createWagmiConfig(customRpcUrls: CustomRpcUrls = {}) { [polygon.id]: http(rpcPolygon), [unichain.id]: http(rpcUnichain), [arbitrum.id]: http(rpcArbitrum), + [etherlink.id]: http(rpcEtherlink), [hyperEvm.id]: http(rpcHyperEVM), [monad.id]: http(rpcMonad), }, diff --git a/src/types/token.ts b/src/types/token.ts index f2743a98..ed4e2f77 100644 --- a/src/types/token.ts +++ b/src/types/token.ts @@ -24,6 +24,7 @@ export const WETH_BY_CHAIN: Partial> = { [SupportedNetworks.Polygon]: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', [SupportedNetworks.Unichain]: '0x4200000000000000000000000000000000000006', [SupportedNetworks.Arbitrum]: '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', + [SupportedNetworks.Etherlink]: '0xfc24f770F94edBca6D6f885E12d4317320BcB401', [SupportedNetworks.Monad]: '0xEE8c0E9f1BFFb4Eb878d8f15f368A02a35481242', }; diff --git a/src/utils/external.ts b/src/utils/external.ts index 159d8288..4967b63b 100644 --- a/src/utils/external.ts +++ b/src/utils/external.ts @@ -1,6 +1,6 @@ import { getNetworkName, SupportedNetworks, getExplorerUrl } from './networks'; -const getMorphoNetworkSlug = (chainId: number): string | undefined => { +export const getMorphoNetworkSlug = (chainId: number): string | undefined => { const network = getNetworkName(chainId)?.toLowerCase(); if (chainId === SupportedNetworks.HyperEVM) { return 'hyperevm'; @@ -8,16 +8,25 @@ const getMorphoNetworkSlug = (chainId: number): string | undefined => { if (chainId === SupportedNetworks.Mainnet) { return 'ethereum'; } + if (chainId === SupportedNetworks.Etherlink) { + return undefined; + } return network; }; +export const supportsMorphoAppLinks = (chainId: number): boolean => { + return getMorphoNetworkSlug(chainId) !== undefined; +}; + export const getMarketURL = (id: string, chainId: number): string => { const network = getMorphoNetworkSlug(chainId); + if (!network) return 'https://app.morpho.org'; return `https://app.morpho.org/${network}/market/${id}`; }; export const getVaultURL = (address: string, chainId: number): string => { const network = getMorphoNetworkSlug(chainId); + if (!network) return 'https://app.morpho.org'; return `https://app.morpho.org/${network}/vault/${address}`; }; diff --git a/src/utils/morpho.ts b/src/utils/morpho.ts index 9c042610..2c88fd1e 100644 --- a/src/utils/morpho.ts +++ b/src/utils/morpho.ts @@ -19,6 +19,8 @@ export const getMorphoAddress = (chain: SupportedNetworks) => { return '0x8f5ae9cddb9f68de460c77730b018ae7e04a140a'; case SupportedNetworks.Arbitrum: return '0x6c247b1F6182318877311737BaC0844bAa518F5e'; + case SupportedNetworks.Etherlink: + return '0xbCE7364E63C3B13C73E9977a83c9704E2aCa876e'; case SupportedNetworks.HyperEVM: return '0x68e37dE8d93d3496ae143F2E900490f6280C57cD'; case SupportedNetworks.Monad: @@ -43,6 +45,8 @@ export const getBundlerV2 = (chain: SupportedNetworks) => { return '0x5738366B9348f22607294007e75114922dF2a16A'; // ChainAgnosticBundlerV2 we deployed case SupportedNetworks.Arbitrum: return '0x5738366B9348f22607294007e75114922dF2a16A'; // ChainAgnosticBundlerV2 we deployed + case SupportedNetworks.Etherlink: + return '0x5738366B9348f22607294007e75114922dF2a16A'; case SupportedNetworks.HyperEVM: return '0x5738366B9348f22607294007e75114922dF2a16A'; // ChainAgnosticBundlerV2 we deployed case SupportedNetworks.Monad: @@ -66,6 +70,8 @@ export const getIRMTitle = (address: string) => { return 'Adaptive Curve'; case '0x66f30587fb8d4206918deb78eca7d5ebbafd06da': // on arbitrum return 'Adaptive Curve'; + case '0xc1523be776e66ba07b609b1914d0925278f21fe5': // on etherlink + return 'Adaptive Curve'; case '0xd4a426f010986dcad727e8dd6eed44ca4a9b7483': // on hyperevm return 'Adaptive Curve'; case '0x09475a3d6ea8c314c592b1a3799bde044e2f400f': // on monad @@ -104,6 +110,8 @@ export function getMorphoGenesisDate(chainId: number): Date { return new Date('2025-02-18T02:03:6.000Z'); case SupportedNetworks.Arbitrum: return new Date('2025-01-17T06:04:51.000Z'); + case SupportedNetworks.Etherlink: + return new Date('2025-07-14T20:41:53.000Z'); case SupportedNetworks.HyperEVM: return new Date('2025-04-03T04:52:00.000Z'); case SupportedNetworks.Monad: diff --git a/src/utils/networks.ts b/src/utils/networks.ts index 46b2ca2c..a9e0bb3f 100644 --- a/src/utils/networks.ts +++ b/src/utils/networks.ts @@ -1,5 +1,15 @@ import { type Address, type Chain, defineChain } from 'viem'; -import { arbitrum, base, mainnet, monad, optimism, polygon, unichain, hyperEvm as hyperEvmOld } from 'viem/chains'; +import { + arbitrum, + base, + etherlink as etherlinkChain, + mainnet, + monad, + optimism, + polygon, + unichain, + hyperEvm as hyperEvmOld, +} from 'viem/chains'; import { v2AgentsBase } from './monarch-agent'; import type { AgentMetadata } from './types'; @@ -13,10 +23,10 @@ const _apiKey = process.env.NEXT_PUBLIC_THEGRAPH_API_KEY; * - If NEXT_PUBLIC_RPC_PRIORITY === 'ALCHEMY': Use Alchemy first, fall back to specific RPC * - Otherwise (default): Use specific network RPC first, fall back to Alchemy */ -const getRpcUrl = (specificRpcUrl: string | undefined, alchemySubdomain: string): string => { +const getRpcUrl = (specificRpcUrl: string | undefined, alchemySubdomain?: string): string => { // Sanitize empty strings to undefined for correct fallback behavior const targetRpc = specificRpcUrl || undefined; - const alchemyUrl = alchemyKey ? `https://${alchemySubdomain}.g.alchemy.com/v2/${alchemyKey}` : undefined; + const alchemyUrl = alchemyKey && alchemySubdomain ? `https://${alchemySubdomain}.g.alchemy.com/v2/${alchemyKey}` : undefined; if (rpcPriority === 'ALCHEMY') { // Prioritize Alchemy when explicitly set @@ -34,6 +44,7 @@ export enum SupportedNetworks { Polygon = 137, Unichain = 130, Arbitrum = 42_161, + Etherlink = 42_793, HyperEVM = 999, Monad = 143, } @@ -45,6 +56,7 @@ export const ALL_SUPPORTED_NETWORKS = [ SupportedNetworks.Polygon, SupportedNetworks.Unichain, SupportedNetworks.Arbitrum, + SupportedNetworks.Etherlink, SupportedNetworks.HyperEVM, SupportedNetworks.Monad, ]; @@ -161,6 +173,18 @@ export const networks: NetworkConfig[] = [ explorerUrl: 'https://arbiscan.io', wrappedNativeToken: '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', }, + { + network: SupportedNetworks.Etherlink, + chain: etherlinkChain, + logo: require('../imgs/chains/etherlink.svg') as string, + name: 'Etherlink', + defaultRPC: getRpcUrl(process.env.NEXT_PUBLIC_ETHERLINK_RPC), + blocktime: 4.83, + maxBlockDelay: 10, + explorerUrl: 'https://explorer.etherlink.com', + nativeTokenSymbol: 'XTZ', + wrappedNativeToken: '0xc9B53AB2679f573e480d01e0f49e2B5CFB7a3EAb', + }, { network: SupportedNetworks.HyperEVM, chain: hyperEvm, diff --git a/src/utils/rpc.ts b/src/utils/rpc.ts index 7acd774e..88e0fcf0 100644 --- a/src/utils/rpc.ts +++ b/src/utils/rpc.ts @@ -1,5 +1,5 @@ import { createPublicClient, http, type PublicClient } from 'viem'; -import { arbitrum, base, mainnet, monad, optimism, polygon, unichain } from 'viem/chains'; +import { arbitrum, base, etherlink, mainnet, monad, optimism, polygon, unichain } from 'viem/chains'; import { getDefaultRPC, getViemChain, SupportedNetworks, hyperEvm } from './networks'; // Default clients (cached) @@ -33,6 +33,10 @@ const initializeDefaultClients = () => { chain: arbitrum, transport: http(getDefaultRPC(SupportedNetworks.Arbitrum)), }) as PublicClient, + [SupportedNetworks.Etherlink]: createPublicClient({ + chain: etherlink, + transport: http(getDefaultRPC(SupportedNetworks.Etherlink)), + }) as PublicClient, [SupportedNetworks.HyperEVM]: createPublicClient({ chain: hyperEvm, transport: http(getDefaultRPC(SupportedNetworks.HyperEVM)), diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts index 850da2b8..bbc749f1 100644 --- a/src/utils/tokens.ts +++ b/src/utils/tokens.ts @@ -1,4 +1,4 @@ -import { type Chain, base, mainnet, polygon, unichain, arbitrum, optimism, monad } from 'viem/chains'; +import { type Chain, arbitrum, base, etherlink, mainnet, monad, optimism, polygon, unichain } from 'viem/chains'; import { getWrappedNativeToken, hyperEvm } from './networks'; export type TokenSource = 'local' | 'external' | 'unknown'; @@ -65,6 +65,7 @@ const supportedTokens = [ chain: arbitrum, address: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', }, + { chain: etherlink, address: '0x796Ea11Fa2dD751eD01b53C372fFDB4AAa8f00F9' }, { chain: hyperEvm, address: '0xb88339cb7199b77e23db6e890353e22632ba630f', @@ -270,6 +271,7 @@ const supportedTokens = [ chain: arbitrum, address: '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', }, + { chain: etherlink, address: '0xfc24f770F94edBca6D6f885E12d4317320BcB401' }, // 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. @@ -284,6 +286,12 @@ const supportedTokens = [ decimals: 18, networks: [{ chain: polygon, address: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270' }], }, + { + symbol: 'WXTZ', + img: undefined, + decimals: 18, + networks: [{ chain: etherlink, address: '0xc9B53AB2679f573e480d01e0f49e2B5CFB7a3EAb' }], + }, { symbol: 'sDAI', img: require('../imgs/tokens/sdai.svg') as string, @@ -372,6 +380,7 @@ const supportedTokens = [ { chain: mainnet, address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599' }, { chain: polygon, address: '0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6' }, { chain: optimism, address: '0x68f180fcCe6836688e9084f035309E29Bf0A2095' }, + { chain: etherlink, address: '0xbFc94CD2B1E55999Cfc7347a9313e88702B83d0F' }, { chain: unichain, address: '0x927B51f251480a681271180DA4de28D44EC4AfB8', @@ -394,6 +403,42 @@ const supportedTokens = [ ], peg: TokenPeg.BTC, }, + { + symbol: 'mBASIS', + img: require('../imgs/tokens/mbasis.png') as string, + decimals: 18, + networks: [{ chain: etherlink, address: '0x2247B5A46BB79421a314aB0f0b67fFd11dd37Ee4' }], + protocol: { + name: 'Midas', + }, + }, + { + symbol: 'xU3O8', + img: require('../imgs/tokens/xu3o8.png') as string, + decimals: 18, + networks: [{ chain: etherlink, address: '0x79052ab3c166d4899a1e0dd033ac3b379af0b1fd' }], + protocol: { + name: 'Midas', + }, + }, + { + symbol: 'mMEV', + img: require('../imgs/tokens/mmev.svg') as string, + decimals: 18, + networks: [{ chain: etherlink, address: '0x5542F82389b76C23f5848268893234d8A63fd5c8' }], + protocol: { + name: 'Midas', + }, + }, + { + symbol: 'mTBILL', + img: require('../imgs/tokens/mtbill.png') as string, + decimals: 18, + networks: [{ chain: etherlink, address: '0xDD629E5241CbC5919847783e6C96B2De4754e438' }], + protocol: { + name: 'Midas', + }, + }, { symbol: 'tBTC', img: require('../imgs/tokens/tbtc.webp') as string,