From c380f89015333356435ed3c7cbddbc80cca51d22 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Tue, 16 Dec 2025 16:00:51 +0800 Subject: [PATCH 1/3] feat: allow chain-specific RPC --- .env.local.example | 13 ++++++- app/api/balances/route.ts | 58 +++----------------------------- src/components/MarketIdBadge.tsx | 1 - src/utils/networks.ts | 22 ++++++++---- 4 files changed, 32 insertions(+), 62 deletions(-) diff --git a/.env.local.example b/.env.local.example index fc851980..75c07fcb 100644 --- a/.env.local.example +++ b/.env.local.example @@ -2,9 +2,20 @@ NEXT_PUBLIC_GOOGLE_ANALYTICS_ID= NEXT_PUBLIC_REOWN_PROJECT_ID= ENVIRONMENT= -NEXT_PUBLIC_ALCHEMY_API_KEY= NEXT_PUBLIC_INFURA_API_KEY= +# If Alchemy API key is not set, Must set the remaining RPC for each chain +NEXT_PUBLIC_ALCHEMY_API_KEY= + +# Individual RPC URLs for each network (optional if ALCHEMY_API_KEY is set) +NEXT_PUBLIC_ETHEREUM_RPC= +NEXT_PUBLIC_BASE_RPC= +NEXT_PUBLIC_POLYGON_RPC= +NEXT_PUBLIC_UNICHAIN_RPC= +NEXT_PUBLIC_ARBITRUM_RPC= +NEXT_PUBLIC_HYPEREVM_RPC= +NEXT_PUBLIC_MONAD_RPC= + NEXT_PUBLIC_THEGRAPH_API_KEY= # Used for balance API diff --git a/app/api/balances/route.ts b/app/api/balances/route.ts index 851aff40..01a80c02 100644 --- a/app/api/balances/route.ts +++ b/app/api/balances/route.ts @@ -1,5 +1,5 @@ import { type NextRequest, NextResponse } from 'next/server'; -import { SupportedNetworks, getDefaultRPC } from '@/utils/networks'; +import { SupportedNetworks } from '@/utils/networks'; import { supportedTokens } from '@/utils/tokens'; import { getKnownBalancesWithClient } from './evm-client'; @@ -19,65 +19,17 @@ export async function GET(req: NextRequest) { try { const chainIdNum = Number(chainId) as SupportedNetworks; - const alchemyUrl = getDefaultRPC(chainIdNum); - if (!alchemyUrl) { - throw new Error(`Chain ${chainId} not supported`); - } + // Get supported token addresses for this chain const tokenAddresses = supportedTokens .filter((token) => token.networks.some((network) => network.chain.id === chainIdNum)) .flatMap((token) => token.networks.filter((network) => network.chain.id === chainIdNum).map((network) => network.address)); - // Special handling for hyper and monad: no alchemy support - if (chainIdNum === SupportedNetworks.HyperEVM || chainIdNum === SupportedNetworks.Monad) { - const tokens = await getKnownBalancesWithClient(address, tokenAddresses, chainIdNum); - return NextResponse.json({ tokens }); - } + // use multicall to query balances at onces + const tokens = await getKnownBalancesWithClient(address, tokenAddresses, chainIdNum); + return NextResponse.json({ tokens }); - // Get token balances for specific tokens only - const balancesResponse = await fetch(alchemyUrl, { - method: 'POST', - headers: { - accept: 'application/json', - 'content-type': 'application/json', - }, - body: JSON.stringify({ - id: 1, - jsonrpc: '2.0', - method: 'alchemy_getTokenBalances', - params: [address, tokenAddresses], - }), - }); - - if (!balancesResponse.ok) { - console.error(`Failed to fetch balances: ${balancesResponse.status} ${balancesResponse.statusText}`); - throw new Error(`HTTP error! status: ${balancesResponse.status}`); - } - - const balancesData = (await balancesResponse.json()) as { - id: number; - jsonrpc: string; - result: { - tokenBalances: TokenBalance[]; - }; - }; - - const nonZeroBalances: TokenBalance[] = balancesData.result.tokenBalances.filter( - (token: TokenBalance) => token.tokenBalance !== '0x0000000000000000000000000000000000000000000000000000000000000000', - ); - - // Filter out failed metadata requests - const tokens = nonZeroBalances - .filter((token) => token !== null) - .map((token) => ({ - address: token.contractAddress.toLowerCase(), - balance: BigInt(token.tokenBalance).toString(10), - })); - - return NextResponse.json({ - tokens, - }); } catch (error) { console.error('Failed to fetch balances:', error); return NextResponse.json({ error: 'Failed to fetch balances' }, { status: 500 }); diff --git a/src/components/MarketIdBadge.tsx b/src/components/MarketIdBadge.tsx index 1e68a038..23ae7829 100644 --- a/src/components/MarketIdBadge.tsx +++ b/src/components/MarketIdBadge.tsx @@ -27,7 +27,6 @@ export function MarketIdBadge({ marketId, chainId, showNetworkIcon = false, show {displayId} diff --git a/src/utils/networks.ts b/src/utils/networks.ts index 9f00da18..d9885115 100644 --- a/src/utils/networks.ts +++ b/src/utils/networks.ts @@ -5,6 +5,14 @@ import type { AgentMetadata } from './types'; const alchemyKey = process.env.NEXT_PUBLIC_ALCHEMY_API_KEY; +/** + * Helper function to get RPC URL with fallback logic. + * Prioritizes specific network RPC URL, falls back to Alchemy if available. + */ +const getRpcUrl = (specificRpcUrl: string | undefined, alchemySubdomain: string): string => { + return specificRpcUrl ?? (alchemyKey ? `https://${alchemySubdomain}.g.alchemy.com/v2/${alchemyKey}` : ''); +}; + export enum SupportedNetworks { Mainnet = 1, Base = 8453, @@ -71,7 +79,7 @@ export const networks: NetworkConfig[] = [ logo: require('../imgs/chains/eth.svg') as string, name: 'Mainnet', chain: mainnet, - defaultRPC: `https://eth-mainnet.g.alchemy.com/v2/${alchemyKey}`, + defaultRPC: getRpcUrl(process.env.NEXT_PUBLIC_ETHEREUM_RPC, 'eth-mainnet'), blocktime: 12, maxBlockDelay: 0, explorerUrl: 'https://etherscan.io', @@ -82,7 +90,7 @@ export const networks: NetworkConfig[] = [ logo: require('../imgs/chains/base.webp') as string, name: 'Base', chain: base, - defaultRPC: `https://base-mainnet.g.alchemy.com/v2/${alchemyKey}`, + defaultRPC: getRpcUrl(process.env.NEXT_PUBLIC_BASE_RPC, 'base-mainnet'), vaultConfig: { v2FactoryAddress: '0x4501125508079A99ebBebCE205DeC9593C2b5857', strategies: v2AgentsBase, @@ -101,7 +109,7 @@ export const networks: NetworkConfig[] = [ chain: polygon, logo: require('../imgs/chains/polygon.png') as string, name: 'Polygon', - defaultRPC: `https://polygon-mainnet.g.alchemy.com/v2/${alchemyKey}`, + defaultRPC: getRpcUrl(process.env.NEXT_PUBLIC_POLYGON_RPC, 'polygon-mainnet'), blocktime: 2, maxBlockDelay: 20, explorerUrl: 'https://polygonscan.com', @@ -112,7 +120,7 @@ export const networks: NetworkConfig[] = [ network: SupportedNetworks.Unichain, chain: unichain, logo: require('../imgs/chains/unichain.svg') as string, - defaultRPC: `https://unichain-mainnet.g.alchemy.com/v2/${alchemyKey}`, + defaultRPC: getRpcUrl(process.env.NEXT_PUBLIC_UNICHAIN_RPC, 'unichain-mainnet'), name: 'Unichain', blocktime: 1, maxBlockDelay: 10, @@ -124,7 +132,7 @@ export const networks: NetworkConfig[] = [ chain: arbitrum, logo: require('../imgs/chains/arbitrum.png') as string, name: 'Arbitrum', - defaultRPC: `https://arb-mainnet.g.alchemy.com/v2/${alchemyKey}`, + defaultRPC: getRpcUrl(process.env.NEXT_PUBLIC_ARBITRUM_RPC, 'arb-mainnet'), blocktime: 2, maxBlockDelay: 2, explorerUrl: 'https://arbiscan.io', @@ -135,7 +143,7 @@ export const networks: NetworkConfig[] = [ chain: hyperEvm, logo: require('../imgs/chains/hyperevm.png') as string, name: 'HyperEVM', - defaultRPC: `https://hyperliquid-mainnet.g.alchemy.com/v2/${alchemyKey}`, + defaultRPC: getRpcUrl(process.env.NEXT_PUBLIC_HYPEREVM_RPC, 'hyperliquid-mainnet'), blocktime: 2, maxBlockDelay: 5, nativeTokenSymbol: 'WHYPE', @@ -147,7 +155,7 @@ export const networks: NetworkConfig[] = [ chain: monad, logo: require('../imgs/chains/monad.svg') as string, name: 'Monad', - defaultRPC: `https://monad-mainnet.g.alchemy.com/v2/${alchemyKey}`, + defaultRPC: getRpcUrl(process.env.NEXT_PUBLIC_MONAD_RPC, 'monad-mainnet'), blocktime: 1, maxBlockDelay: 5, nativeTokenSymbol: 'MON', From e13d9c34762cc8fb57102ed53fc12ce1c57d59f2 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Tue, 16 Dec 2025 16:23:13 +0800 Subject: [PATCH 2/3] chore: allow setting flags --- .env.local.example | 15 +++++++++++++-- app/api/balances/route.ts | 11 ++--------- .../[chainId]/[marketid]/VolumeChart.tsx | 4 ++-- src/utils/networks.ts | 18 +++++++++++++++--- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/.env.local.example b/.env.local.example index 75c07fcb..0d9e36ef 100644 --- a/.env.local.example +++ b/.env.local.example @@ -4,10 +4,18 @@ ENVIRONMENT= NEXT_PUBLIC_INFURA_API_KEY= -# If Alchemy API key is not set, Must set the remaining RPC for each chain +# ==================== RPC Settings ==================== + +# RPC Priority: Set to "ALCHEMY" to prioritize Alchemy API when both are available +# Default (or any other value): Prioritizes individual network RPCs first +NEXT_PUBLIC_RPC_PRIORITY="ALCHEMY" + +# Must set either NEXT_PUBLIC_ALCHEMY_API_KEY or individual RPC URLS for all networks + +# Alchemy API Key for all networks enabled in Alchemy project settings NEXT_PUBLIC_ALCHEMY_API_KEY= -# Individual RPC URLs for each network (optional if ALCHEMY_API_KEY is set) +# Individual RPC URLs NEXT_PUBLIC_ETHEREUM_RPC= NEXT_PUBLIC_BASE_RPC= NEXT_PUBLIC_POLYGON_RPC= @@ -16,6 +24,9 @@ NEXT_PUBLIC_ARBITRUM_RPC= NEXT_PUBLIC_HYPEREVM_RPC= NEXT_PUBLIC_MONAD_RPC= + +# ==================== End of RPC Settings ==================== + NEXT_PUBLIC_THEGRAPH_API_KEY= # Used for balance API diff --git a/app/api/balances/route.ts b/app/api/balances/route.ts index 01a80c02..8e2e2d19 100644 --- a/app/api/balances/route.ts +++ b/app/api/balances/route.ts @@ -1,13 +1,8 @@ import { type NextRequest, NextResponse } from 'next/server'; -import { SupportedNetworks } from '@/utils/networks'; +import type { SupportedNetworks } from '@/utils/networks'; import { supportedTokens } from '@/utils/tokens'; import { getKnownBalancesWithClient } from './evm-client'; -type TokenBalance = { - contractAddress: string; - tokenBalance: string; -}; - export async function GET(req: NextRequest) { const searchParams = req.nextUrl.searchParams; const address = searchParams.get('address'); @@ -19,17 +14,15 @@ export async function GET(req: NextRequest) { try { const chainIdNum = Number(chainId) as SupportedNetworks; - // Get supported token addresses for this chain const tokenAddresses = supportedTokens .filter((token) => token.networks.some((network) => network.chain.id === chainIdNum)) .flatMap((token) => token.networks.filter((network) => network.chain.id === chainIdNum).map((network) => network.address)); - // use multicall to query balances at onces + // use multicall to query balances at once const tokens = await getKnownBalancesWithClient(address, tokenAddresses, chainIdNum); return NextResponse.json({ tokens }); - } catch (error) { console.error('Failed to fetch balances:', error); return NextResponse.json({ error: 'Failed to fetch balances' }, { status: 500 }); diff --git a/app/market/[chainId]/[marketid]/VolumeChart.tsx b/app/market/[chainId]/[marketid]/VolumeChart.tsx index 1dd650c3..489edf9b 100644 --- a/app/market/[chainId]/[marketid]/VolumeChart.tsx +++ b/app/market/[chainId]/[marketid]/VolumeChart.tsx @@ -299,7 +299,7 @@ function VolumeChart({
-

Current Volumes

+

Current Volumes

{['supply', 'borrow', 'liquidity'].map((type) => { const stats = getCurrentVolumeStats(type as 'supply' | 'borrow' | 'liquidity'); return ( @@ -321,7 +321,7 @@ function VolumeChart({
-

+

Historical Averages ({selectedTimeframe})

{isLoading ? ( diff --git a/src/utils/networks.ts b/src/utils/networks.ts index d9885115..7cda54d5 100644 --- a/src/utils/networks.ts +++ b/src/utils/networks.ts @@ -4,13 +4,25 @@ import { v2AgentsBase } from './monarch-agent'; import type { AgentMetadata } from './types'; const alchemyKey = process.env.NEXT_PUBLIC_ALCHEMY_API_KEY; +const rpcPriority = process.env.NEXT_PUBLIC_RPC_PRIORITY; /** - * Helper function to get RPC URL with fallback logic. - * Prioritizes specific network RPC URL, falls back to Alchemy if available. + * Helper function to get RPC URL with fallback logic. Priority behavior: + * - 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 => { - return specificRpcUrl ?? (alchemyKey ? `https://${alchemySubdomain}.g.alchemy.com/v2/${alchemyKey}` : ''); + // Sanitize empty strings to undefined for correct fallback behavior + const targetRpc = specificRpcUrl || undefined; + const alchemyUrl = alchemyKey ? `https://${alchemySubdomain}.g.alchemy.com/v2/${alchemyKey}` : undefined; + + if (rpcPriority === 'ALCHEMY') { + // Prioritize Alchemy when explicitly set + return alchemyUrl ?? targetRpc ?? ''; + } + + // Default: prioritize specific network RPC + return targetRpc ?? alchemyUrl ?? ''; }; export enum SupportedNetworks { From 20fb6fe3f20daa459ce3b85837cfa23860ed5626 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Tue, 16 Dec 2025 16:28:06 +0800 Subject: [PATCH 3/3] chore: lint --- .env.local.example | 2 +- app/market/[chainId]/[marketid]/RateChart.tsx | 6 +++--- app/market/[chainId]/[marketid]/VolumeChart.tsx | 6 +++--- docs/Styling.md | 4 ---- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.env.local.example b/.env.local.example index 0d9e36ef..691db78d 100644 --- a/.env.local.example +++ b/.env.local.example @@ -8,7 +8,7 @@ NEXT_PUBLIC_INFURA_API_KEY= # RPC Priority: Set to "ALCHEMY" to prioritize Alchemy API when both are available # Default (or any other value): Prioritizes individual network RPCs first -NEXT_PUBLIC_RPC_PRIORITY="ALCHEMY" +NEXT_PUBLIC_RPC_PRIORITY=ALCHEMY # Must set either NEXT_PUBLIC_ALCHEMY_API_KEY or individual RPC URLS for all networks diff --git a/app/market/[chainId]/[marketid]/RateChart.tsx b/app/market/[chainId]/[marketid]/RateChart.tsx index e94bc21f..9bcd2ec5 100644 --- a/app/market/[chainId]/[marketid]/RateChart.tsx +++ b/app/market/[chainId]/[marketid]/RateChart.tsx @@ -258,7 +258,7 @@ function RateChart({ historicalData, market, isLoading, selectedTimeframe, selec
-

Current Rates

+

Current Rates

-

- Historical Averages ({selectedTimeframe}) +

+ Historical Averages ({selectedTimeframe})

{isLoading ? (
diff --git a/app/market/[chainId]/[marketid]/VolumeChart.tsx b/app/market/[chainId]/[marketid]/VolumeChart.tsx index 489edf9b..9ad7e832 100644 --- a/app/market/[chainId]/[marketid]/VolumeChart.tsx +++ b/app/market/[chainId]/[marketid]/VolumeChart.tsx @@ -299,7 +299,7 @@ function VolumeChart({
-

Current Volumes

+

Current Volumes

{['supply', 'borrow', 'liquidity'].map((type) => { const stats = getCurrentVolumeStats(type as 'supply' | 'borrow' | 'liquidity'); return ( @@ -321,8 +321,8 @@ function VolumeChart({
-

- Historical Averages ({selectedTimeframe}) +

+ Historical Averages ({selectedTimeframe})

{isLoading ? (
diff --git a/docs/Styling.md b/docs/Styling.md index 5572674a..fd9f4e04 100644 --- a/docs/Styling.md +++ b/docs/Styling.md @@ -226,10 +226,6 @@ For section headers within modal content, use consistent styling: ```tsx // ✅ Correct

Section Title

- -// ❌ Incorrect -

Section Title

-

Section Title

``` ### Custom Modals (Non-HeroUI)