diff --git a/components/AddressBar.tsx b/components/AddressBar.tsx index 4f8912b..feb0fa0 100644 --- a/components/AddressBar.tsx +++ b/components/AddressBar.tsx @@ -1,19 +1,30 @@ "use client"; -import { useDisconnect } from "@starknet-react/core"; +import { useDisconnect, useNetwork } from "@starknet-react/core"; import React, { useMemo, useState } from "react"; import ClickOutsideWrapper from "./outsideClick"; import { Copy, LogOut, SquareArrowUpRight, Check } from "lucide-react"; +import { useTokenBalance } from "@/hooks/useTokenBalance"; +import { getNetworkInfo } from "@/utils/networkUtils"; function AddressBar({ address }: { address: string }) { const { disconnect } = useDisconnect(); + const { chain } = useNetwork(); const [showDisconnect, setShowDisconnect] = useState(false); const [copied, setCopied] = useState(false); + const { formattedBalance, isLoading: balanceLoading, isError: balanceError, isAccountDeployed } = useTokenBalance(); + const shortenedAddress = useMemo( () => (address ? `${address.slice(0, 6)}...${address.slice(-4)}` : ""), [address] ); + + const { isMainnet, isTestnet, networkName: currentNetworkName, explorerUrl } = useMemo( + () => getNetworkInfo(chain?.id, address), + [chain?.id, address] + ); + const handleCopy = async () => { try { await navigator.clipboard.writeText(address); @@ -27,58 +38,87 @@ function AddressBar({ address }: { address: string }) { return ( <> setShowDisconnect((prev) => !prev)} > - {shortenedAddress} + {shortenedAddress} + + {balanceLoading ? "Loading..." : + balanceError && isAccountDeployed === false ? `Deploy on ${currentNetworkName.toLowerCase()}` : + balanceError ? "Error" : `${formattedBalance} ETH`} + {showDisconnect && ( setShowDisconnect(false)} - className="absolute top-[100%] right-[1rem] pl-4 pr-4 pt-4 pb-2 rounded bg-white w-[40%] z-10 shadow-lg" + className="absolute top-[100%] right-[1rem] pl-4 pr-4 pt-4 pb-2 rounded bg-white dark:bg-dark-surface w-[40%] z-10 shadow-lg dark:shadow-dark-border/20 border border-gray-200 dark:border-dark-border transition-colors duration-300" > - - + + Wallet setShowDisconnect(false)} > X - - + + Connected disconnect()} - className="flex items-center gap-x-1" + className="flex items-center gap-x-1 hover:text-primaryColor dark:hover:text-dark-accent transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-primaryColor dark:focus:ring-dark-accent rounded px-1" > Disconnect + + {/* Network section */} + + + + {currentNetworkName} + + - + - {shortenedAddress} + + {shortenedAddress} + + {balanceLoading ? "Loading balance..." : + balanceError && isAccountDeployed === false ? `Not deployed on ${currentNetworkName.toLowerCase()}` : + balanceError ? "Balance unavailable" : `${formattedBalance} ETH`} + + - + + {/* Account deployment info */} + {balanceError && isAccountDeployed === false && ( + + + {currentNetworkName} Deployment Required: Your account needs to be deployed on {isMainnet ? 'Starknet mainnet' : 'Sepolia testnet'}. Make your first transaction on {currentNetworkName.toLowerCase()} to deploy the account here. + + + )} + View on explorer {copied ? ( <> diff --git a/components/StarknetProvider.tsx b/components/StarknetProvider.tsx index 2431224..4d6bcca 100644 --- a/components/StarknetProvider.tsx +++ b/components/StarknetProvider.tsx @@ -7,8 +7,11 @@ import { } from "@starknet-react/core"; import ControllerConnector from "@cartridge/connector/controller"; + +export const NETWORK: "sepolia" | "mainnet" = "sepolia"; // "sepolia" or "mainnet" + // Define your contract addresses -const ETH_TOKEN_ADDRESS: `0x${string}` = +export const ETH_TOKEN_ADDRESS: `0x${string}` = "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"; // Define session policies type inline @@ -29,6 +32,7 @@ const policies = { // Define custom chain objects with rpcUrl and rpcUrls properties +// Chains const mainnet = { id: BigInt(1), network: "mainnet", @@ -79,12 +83,11 @@ const sepolia = { }, }; -// Initialize the connector outside of any React component -const connector = new ControllerConnector({ - policies, - chains: [mainnet, sepolia], - defaultChainId: sepolia.id.toString(), -}); +// Chains for Cartridge Controller +const controllerChains = [ + { ...mainnet, id: "0x534e5f4d41494e" }, // SN_MAIN in hex + { ...sepolia, id: "0x534e5f5345504f4c4941" } // SN_SEPOLIA in hex +]; // Configure RPC provider const provider = jsonRpcProvider({ @@ -99,14 +102,32 @@ const provider = jsonRpcProvider({ }, }); +// Lazy connector initialization to avoid SSR issues +let connector: ControllerConnector | null = null; + +function getConnector() { + if (!connector && typeof window !== 'undefined') { + connector = new ControllerConnector({ + policies, + chains: controllerChains, + defaultChainId: NETWORK === "mainnet" ? "0x534e5f4d41494e" : "0x534e5f5345504f4c4941", + }); + } + return connector; +} + export function StarknetProvider({ children }: { children: React.ReactNode }) { + const defaultChainId = NETWORK === "mainnet" ? mainnet.id : sepolia.id; + const connectorInstance = getConnector(); + return ( {children} diff --git a/components/TotalTransactionChart.tsx b/components/TotalTransactionChart.tsx index a68f75e..1289d17 100644 --- a/components/TotalTransactionChart.tsx +++ b/components/TotalTransactionChart.tsx @@ -9,6 +9,7 @@ import { Legend, } from "recharts"; import { useTheme } from "next-themes"; +import { useState, useEffect } from "react"; const data = [ { name: "JAN", transactions: 1000 }, @@ -22,6 +23,21 @@ const data = [ const TotalTransactionChart = () => { const { theme } = useTheme(); const isDark = theme === "dark"; + const [windowWidth, setWindowWidth] = useState(1024); // Default to desktop size + + useEffect(() => { + // Only access window on client side + if (typeof window !== 'undefined') { + setWindowWidth(window.innerWidth); + + const handleResize = () => { + setWindowWidth(window.innerWidth); + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + } + }, []); return ( @@ -40,7 +56,7 @@ const TotalTransactionChart = () => { axisLine={false} tick={{ fill: isDark ? "#B8A8CC" : "#666666", - fontSize: window.innerWidth < 640 ? 10 : 12, + fontSize: windowWidth < 640 ? 10 : 12, }} interval={0} /> @@ -49,9 +65,9 @@ const TotalTransactionChart = () => { tickLine={false} tick={{ fill: isDark ? "#B8A8CC" : "#666666", - fontSize: window.innerWidth < 640 ? 10 : 12, + fontSize: windowWidth < 640 ? 10 : 12, }} - width={window.innerWidth < 640 ? 40 : 60} + width={windowWidth < 640 ? 40 : 60} /> { { dataKey="transactions" fill={isDark ? "#9D7BEA" : "#2D0561"} barSize={ - window.innerWidth < 640 ? 20 : window.innerWidth < 1024 ? 25 : 30 + windowWidth < 640 ? 20 : windowWidth < 1024 ? 25 : 30 } name="2022" radius={[2, 2, 0, 0]} diff --git a/components/modal-component/PaymentModal.tsx b/components/modal-component/PaymentModal.tsx index 560aa7b..6c8fb02 100644 --- a/components/modal-component/PaymentModal.tsx +++ b/components/modal-component/PaymentModal.tsx @@ -70,12 +70,12 @@ const PaymentModal: React.FC = ({ onNext, onPrevious, closeModal }) const salt = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)); const calls = useMemo(() => { - const isInputValid = user && localPaymentAmount !== "" || false && localSelectedPaymentStructure !== "" && paymentAmount !== "" || undefined; + const isInputValid = user && localPaymentAmount !== "" && localSelectedPaymentStructure !== "" && paymentAmount !== ""; - if (!isInputValid || !contract ) return + if (!isInputValid || !contract || !user) return; - return [contract?.populate("deploy_escrow", [user, beneficiary2, arbiter, salt])] - }, [localPaymentAmount, localSelectedPaymentStructure, paymentAmount]) + return [contract.populate("deploy_escrow", [user, beneficiary2, arbiter, salt])] + }, [user, localPaymentAmount, localSelectedPaymentStructure, paymentAmount, contract, beneficiary2, arbiter, salt]) console.log(calls) diff --git a/hooks/useTokenBalance.ts b/hooks/useTokenBalance.ts new file mode 100644 index 0000000..b0b3704 --- /dev/null +++ b/hooks/useTokenBalance.ts @@ -0,0 +1,147 @@ +import { useAccount, useContract, useProvider, useNetwork } from "@starknet-react/core"; +import { useMemo, useEffect, useState } from "react"; +import { ETH_TOKEN_ADDRESS } from "@/components/StarknetProvider"; + +// ERC20 ABI for balanceOf function +const ERC20_ABI = [ + { + name: "balanceOf", + type: "function" as const, + inputs: [ + { + name: "account", + type: "felt" + } + ], + outputs: [ + { + name: "balance", + type: "Uint256" + } + ], + state_mutability: "view" as const + } +] as const; + +export function useTokenBalance() { + const { address, isConnected, account } = useAccount(); + const { provider } = useProvider(); + const { chain } = useNetwork(); + const [balance, setBalance] = useState(BigInt(0)); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + const [error, setError] = useState(null); + const [isAccountDeployed, setIsAccountDeployed] = useState(null); + + const { contract } = useContract({ + abi: ERC20_ABI, + address: ETH_TOKEN_ADDRESS, + }); + + useEffect(() => { + if (!address || !isConnected || !contract || !provider) { + setBalance(BigInt(0)); + setIsAccountDeployed(null); + return; + } + + const checkAccountAndFetchBalance = async () => { + setIsLoading(true); + setIsError(false); + setError(null); + + try { + // Check if the account contract is deployed on the current network using provider + let classHash; + try { + classHash = await provider.getClassHashAt(address); + } catch (deploymentError) { + // If we can't get the class hash, the account is not deployed on this network + setIsAccountDeployed(false); + setBalance(BigInt(0)); + setIsError(true); + setError(new Error("Account not deployed on this network")); + return; + } + + if (!classHash || classHash === "0x0") { + setIsAccountDeployed(false); + setBalance(BigInt(0)); + setIsError(true); + setError(new Error("Account not deployed on this network")); + return; + } + + setIsAccountDeployed(true); + + // If account is deployed on current network, fetch balance + const result = await contract.balanceOf(address); + const resultObj = result as { balance: bigint }; + setBalance(resultObj.balance); + } catch (err) { + const errorMessage = (err as Error).message || "Unknown error"; + + // Check if the error is related to undeployed account + if (errorMessage.includes("Contract not found") || + errorMessage.includes("is not deployed") || + errorMessage.includes("Class with hash") || + errorMessage.includes("ContractNotFound") || + errorMessage.includes("StarknetErrorCode.UNINITIALIZED_CONTRACT")) { + setIsAccountDeployed(false); + setError(new Error("Account not deployed on this network")); + } else { + setIsAccountDeployed(true); + setError(err as Error); + } + + setIsError(true); + setBalance(BigInt(0)); + } finally { + setIsLoading(false); + } + }; + + checkAccountAndFetchBalance(); + + // Set up polling for real-time updates every 30 seconds + const interval = setInterval(checkAccountAndFetchBalance, 30000); + return () => clearInterval(interval); + }, [address, isConnected, contract, provider, chain?.id]); + + const formattedBalance = useMemo(() => { + if (isError) { + if (isAccountDeployed === false) { + return "0.00"; // Show 0.00 for undeployed accounts + } + return "0.00"; + } + + if (balance === BigInt(0)) { + return "0.00"; + } + + // Convert from wei to ETH (18 decimals) using BigInt arithmetic + const divisor = BigInt(10) ** BigInt(18); + const ethWhole = balance / divisor; + const ethRemainder = balance % divisor; + + // Convert to decimal + const ethBalance = Number(ethWhole) + Number(ethRemainder) / Number(divisor); + + // Format with appropriate decimal places + const formatted = ethBalance < 0.0001 + ? ethBalance.toExponential(2) + : ethBalance.toFixed(4); + + return parseFloat(formatted).toString(); + }, [balance, isError, isAccountDeployed]); + + return { + balance, + formattedBalance, + isLoading, + isError, + error, + isAccountDeployed + }; +} \ No newline at end of file diff --git a/utils/networkUtils.ts b/utils/networkUtils.ts new file mode 100644 index 0000000..eb51c6e --- /dev/null +++ b/utils/networkUtils.ts @@ -0,0 +1,57 @@ +export interface NetworkInfo { + isMainnet: boolean; + isTestnet: boolean; + networkName: string; + explorerUrl: string; +} + +/** + * Get comprehensive network information based on chain ID + */ +export function getNetworkInfo(chainId: bigint | undefined, address: string): NetworkInfo { + const isMainnet = chainId === BigInt(1); + const isTestnet = chainId === BigInt(2); + const networkName = isMainnet ? "Mainnet" : "Testnet"; + + const explorerUrl = isMainnet + ? `https://voyager.online/contract/${address}` + : `https://sepolia.voyager.online/contract/${address}`; + + return { + isMainnet, + isTestnet, + networkName, + explorerUrl + }; +} + +/** + * Check if current network is mainnet + */ +export function isMainnetNetwork(chainId: bigint | undefined): boolean { + return chainId === BigInt(1); +} + +/** + * Check if current network is testnet + */ +export function isTestnetNetwork(chainId: bigint | undefined): boolean { + return chainId === BigInt(2); +} + +/** + * Get network display name + */ +export function getNetworkName(chainId: bigint | undefined): string { + return isMainnetNetwork(chainId) ? "Mainnet" : "Testnet"; +} + +/** + * Get explorer URL for address on current network + */ +export function getExplorerUrl(chainId: bigint | undefined, address: string): string { + return isMainnetNetwork(chainId) + ? `https://voyager.online/contract/${address}` + : `https://sepolia.voyager.online/contract/${address}`; +} +
+
Wallet
+ {currentNetworkName} Deployment Required: Your account needs to be deployed on {isMainnet ? 'Starknet mainnet' : 'Sepolia testnet'}. Make your first transaction on {currentNetworkName.toLowerCase()} to deploy the account here. +