diff --git a/frontend/lib/contracts/community.ts b/frontend/lib/contracts/community.ts index a96317d..29f69b1 100644 --- a/frontend/lib/contracts/community.ts +++ b/frontend/lib/contracts/community.ts @@ -1,7 +1,7 @@ -import { createPublicClient, createWalletClient, custom, http } from 'viem'; -import { BASE_CHAIN, ENVIRONMENT } from './config'; -import { getCommunity, Community } from './registry'; -import { createResilientPublicClient, cachedContractRead } from './rpc-client'; +import { createPublicClient, createWalletClient, custom, http } from "viem"; +import { BASE_CHAIN, ENVIRONMENT } from "./config"; +import { getCommunity, Community } from "./registry"; +import { createResilientPublicClient } from "./rpc-client"; // AccessNFT ABI - key functions for checking balance and minting const ACCESS_NFT_ABI = [ @@ -37,7 +37,13 @@ const ACCESS_NFT_ABI = [ { inputs: [], name: "priceFeed", - outputs: [{ name: "", type: "address", internalType: "contract AggregatorV3Interface" }], + outputs: [ + { + name: "", + type: "address", + internalType: "contract AggregatorV3Interface", + }, + ], stateMutability: "view", type: "function", }, @@ -62,11 +68,11 @@ const ACCESS_NFT_ABI = [ outputs: [{ name: "", type: "string", internalType: "string" }], stateMutability: "view", type: "function", - } + }, ] as const; // User role types -export type UserRole = 'creator' | 'visitor'; +export type UserRole = "creator" | "visitor"; export interface CommunityData extends Community { nftName?: string; @@ -88,112 +94,83 @@ export interface UserCommunityStatus { * @param communityId - The community ID to fetch * @returns Promise - Enhanced community data or null */ -export async function getCommunityData(communityId: bigint): Promise { +export async function getCommunityData( + communityId: bigint +): Promise { try { const community = await getCommunity(communityId); if (!community) return null; - const publicClient = createPublicClient({ - chain: BASE_CHAIN, - transport: http(ENVIRONMENT.RPC_URL), + const publicClient = createResilientPublicClient(); + const nftAddress = community.nft as `0x${string}`; + + // Use multicall to batch all reads into a single RPC call + // This reduces 5 separate RPC calls to just 1 call + const results = await publicClient.multicall({ + contracts: [ + { + address: nftAddress, + abi: ACCESS_NFT_ABI, + functionName: "name", + }, + { + address: nftAddress, + abi: ACCESS_NFT_ABI, + functionName: "symbol", + }, + { + address: nftAddress, + abi: ACCESS_NFT_ABI, + functionName: "getMintPrice", + }, + { + address: nftAddress, + abi: ACCESS_NFT_ABI, + functionName: "mintPriceUSD", + }, + { + address: nftAddress, + abi: ACCESS_NFT_ABI, + functionName: "priceFeed", + }, + ], + allowFailure: true, // Allow optional fields to fail gracefully }); - // Helper function to check if error is a rate limit error - const isRateLimitError = (error: unknown): boolean => { - if (error instanceof Error) { - return error.message.includes('429') || - error.message.includes('rate limit') || - error.message.includes('over rate limit'); - } - const errorCode = (error as { code?: number })?.code; - return errorCode === -32016; - }; - - // Fetch essential NFT details first (name and symbol are required) - // Use sequential calls with delays to avoid rate limits - let nftName: string; - let nftSymbol: string; - - try { - nftName = await publicClient.readContract({ - address: community.nft as `0x${string}`, - abi: ACCESS_NFT_ABI, - functionName: 'name', - }) as string; - - // Small delay between calls - await new Promise(resolve => setTimeout(resolve, 100)); - - nftSymbol = await publicClient.readContract({ - address: community.nft as `0x${string}`, - abi: ACCESS_NFT_ABI, - functionName: 'symbol', - }) as string; - } catch (error) { - if (isRateLimitError(error)) { - throw new Error('Rate limit exceeded while fetching essential NFT data. Please try again later.'); - } - throw error; - } - - // Fetch pricing data with error handling - these are optional and can fail due to rate limits - let mintPrice: bigint | undefined; - let mintPriceUSD: bigint | undefined; - let priceFeedAddress: string | undefined; - - // Add delays between optional calls to reduce rate limit pressure - await new Promise(resolve => setTimeout(resolve, 150)); - - try { - mintPrice = await publicClient.readContract({ - address: community.nft as `0x${string}`, - abi: ACCESS_NFT_ABI, - functionName: 'getMintPrice', - }) as bigint; - } catch (error) { - console.warn('Failed to fetch mint price (may be rate limited):', error); - // Continue without mint price - } - - await new Promise(resolve => setTimeout(resolve, 150)); - - try { - mintPriceUSD = await publicClient.readContract({ - address: community.nft as `0x${string}`, - abi: ACCESS_NFT_ABI, - functionName: 'mintPriceUSD', - }) as bigint; - } catch (error) { - console.warn('Failed to fetch mint price USD (may be rate limited):', error); - // Continue without USD price - } - - await new Promise(resolve => setTimeout(resolve, 150)); - - try { - priceFeedAddress = await publicClient.readContract({ - address: community.nft as `0x${string}`, - abi: ACCESS_NFT_ABI, - functionName: 'priceFeed', - }) as string; - } catch (error) { - console.warn('Failed to fetch price feed address (may be rate limited):', error); - // Continue without price feed address + const [ + nameResult, + symbolResult, + mintPriceResult, + mintPriceUSDResult, + priceFeedResult, + ] = results; + + // Extract results (name and symbol are required) + if (nameResult.status === "failure" || symbolResult.status === "failure") { + throw new Error("Failed to fetch essential NFT data"); } return { ...community, - nftName, - nftSymbol, - mintPrice, - mintPriceUSD, - priceFeedAddress, + nftName: nameResult.result as string, + nftSymbol: symbolResult.result as string, + mintPrice: + mintPriceResult.status === "success" + ? (mintPriceResult.result as bigint) + : undefined, + mintPriceUSD: + mintPriceUSDResult.status === "success" + ? (mintPriceUSDResult.result as bigint) + : undefined, + priceFeedAddress: + priceFeedResult.status === "success" + ? (priceFeedResult.result as string) + : undefined, }; } catch (error) { - console.error('Failed to fetch community data:', error); + console.error("Failed to fetch community data:", error); - // Re-throw rate limit errors so they can be handled properly by the UI - if (error instanceof Error && error.message.includes('Rate limit')) { + if (error instanceof Error && error.message.includes("Rate limit")) { throw error; } @@ -207,7 +184,10 @@ export async function getCommunityData(communityId: bigint): Promise - The user's NFT balance */ -export async function checkNFTBalance(userAddress: string, nftAddress: string): Promise { +export async function checkNFTBalance( + userAddress: string, + nftAddress: string +): Promise { const publicClient = createPublicClient({ chain: BASE_CHAIN, transport: http(ENVIRONMENT.RPC_URL), @@ -219,9 +199,11 @@ export async function checkNFTBalance(userAddress: string, nftAddress: string): // Helper function to check if error is a rate limit error const isRateLimitError = (error: unknown): boolean => { if (error instanceof Error) { - return error.message.includes('429') || - error.message.includes('rate limit') || - error.message.includes('over rate limit'); + return ( + error.message.includes("429") || + error.message.includes("rate limit") || + error.message.includes("over rate limit") + ); } const errorCode = (error as { code?: number })?.code; return errorCode === -32016; @@ -233,7 +215,7 @@ export async function checkNFTBalance(userAddress: string, nftAddress: string): const balance = await publicClient.readContract({ address: nftAddress as `0x${string}`, abi: ACCESS_NFT_ABI, - functionName: 'balanceOf', + functionName: "balanceOf", args: [userAddress as `0x${string}`], }); @@ -244,9 +226,11 @@ export async function checkNFTBalance(userAddress: string, nftAddress: string): // If it's the last attempt or not a rate limit error, return 0 if (attempt === MAX_RETRIES - 1 || !isRateLimit) { if (isRateLimit) { - console.warn('Rate limit hit when checking NFT balance, returning 0. Please try again later.'); + console.warn( + "Rate limit hit when checking NFT balance, returning 0. Please try again later." + ); } else { - console.error('Failed to check NFT balance:', error); + console.error("Failed to check NFT balance:", error); } // Return 0 as default (user doesn't have NFT) return BigInt(0); @@ -254,10 +238,14 @@ export async function checkNFTBalance(userAddress: string, nftAddress: string): // Calculate exponential backoff delay const delay = INITIAL_DELAY * Math.pow(2, attempt); - console.warn(`Rate limit error when checking NFT balance (attempt ${attempt + 1}/${MAX_RETRIES}), retrying in ${delay}ms...`); + console.warn( + `Rate limit error when checking NFT balance (attempt ${ + attempt + 1 + }/${MAX_RETRIES}), retrying in ${delay}ms...` + ); // Wait before retrying - await new Promise(resolve => setTimeout(resolve, delay)); + await new Promise((resolve) => setTimeout(resolve, delay)); } } @@ -271,7 +259,10 @@ export async function checkNFTBalance(userAddress: string, nftAddress: string): * @param creatorAddress - The community creator's address * @returns boolean - True if the user is the creator */ -export function isCreator(userAddress: string, creatorAddress: string): boolean { +export function isCreator( + userAddress: string, + creatorAddress: string +): boolean { return userAddress.toLowerCase() === creatorAddress.toLowerCase(); } @@ -288,16 +279,16 @@ export async function getUserCommunityStatus( try { const [nftBalance, userIsCreator] = await Promise.all([ checkNFTBalance(userAddress, community.nft), - Promise.resolve(isCreator(userAddress, community.creator)) + Promise.resolve(isCreator(userAddress, community.creator)), ]); const hasNFT = nftBalance > BigInt(0); let role: UserRole; if (userIsCreator) { - role = 'creator'; + role = "creator"; } else { - role = 'visitor'; + role = "visitor"; } return { @@ -307,9 +298,9 @@ export async function getUserCommunityStatus( canVote: hasNFT || userIsCreator, // Both NFT holders and creators can vote }; } catch (error) { - console.error('Failed to get user community status:', error); + console.error("Failed to get user community status:", error); return { - role: 'visitor', + role: "visitor", nftBalance: BigInt(0), isCreator: false, canVote: false, @@ -325,8 +316,8 @@ export async function getUserCommunityStatus( export async function mintNFT(nftAddress: string): Promise { try { // Check if MetaMask is available - if (typeof window === 'undefined' || !window.ethereum) { - throw new Error('MetaMask not found. Please install MetaMask.'); + if (typeof window === "undefined" || !window.ethereum) { + throw new Error("MetaMask not found. Please install MetaMask."); } // Create clients @@ -343,35 +334,37 @@ export async function mintNFT(nftAddress: string): Promise { // Get the connected account const [account] = await walletClient.getAddresses(); if (!account) { - throw new Error('No wallet connected'); + throw new Error("No wallet connected"); } // Get current mint price from contract - const mintPrice = await publicClient.readContract({ + const mintPrice = (await publicClient.readContract({ address: nftAddress as `0x${string}`, abi: ACCESS_NFT_ABI, - functionName: 'getMintPrice', - }) as bigint; + functionName: "getMintPrice", + })) as bigint; // Execute the mint transaction with dynamic price const hash = await walletClient.writeContract({ address: nftAddress as `0x${string}`, abi: ACCESS_NFT_ABI, - functionName: 'mint', + functionName: "mint", account, value: mintPrice, }); return hash; } catch (error) { - console.error('Failed to mint NFT:', error); + console.error("Failed to mint NFT:", error); // Handle specific errors if (error instanceof Error) { - if (error.message.includes('InvalidPriceFeed')) { - throw new Error('Price feed unavailable. Please try again later.'); - } else if (error.message.includes('InvalidMintValue')) { - throw new Error('Incorrect payment amount. Please refresh and try again.'); + if (error.message.includes("InvalidPriceFeed")) { + throw new Error("Price feed unavailable. Please try again later."); + } else if (error.message.includes("InvalidMintValue")) { + throw new Error( + "Incorrect payment amount. Please refresh and try again." + ); } } @@ -394,12 +387,12 @@ export async function getMintPrice(nftAddress: string): Promise { const price = await publicClient.readContract({ address: nftAddress as `0x${string}`, abi: ACCESS_NFT_ABI, - functionName: 'getMintPrice', + functionName: "getMintPrice", }); return price as bigint; } catch (error) { - console.error('Failed to get mint price:', error); + console.error("Failed to get mint price:", error); return BigInt(0); } }