Skip to content
Merged
34 changes: 34 additions & 0 deletions .claude/rules/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,37 @@ OpenScan includes special support for localhost development:
- Use `--overlay-light-*` variables for backgrounds that need to work in both themes
- Use `--text-primary`, `--text-secondary`, `--text-tertiary` for text colors
- Never hardcode `rgba(255, 255, 255, X)` - use CSS variables instead

## Logger Utility

Always use the logger utility instead of `console.*` methods for runtime logging.

### Usage

```typescript
import { logger } from "../utils/logger";

logger.debug("Debug info for development");
logger.info("General information");
logger.warn("Warning message");
logger.error("Error message", error);
```

### Log Level Guidelines

- **debug**: Development-only information (verbose data, state dumps, request/response details)
- **info**: General operational information (feature activity, successful operations)
- **warn**: Potential issues that don't break functionality (fallbacks, deprecations, recoverable errors)
- **error**: Actual errors that affect functionality (failed operations, exceptions)

### Environment Filtering

| Environment | Visible Levels |
|-------------|--------------------------|
| development | debug, info, warn, error |
| staging | info, warn, error |
| production | warn, error |

### Build-time Safety

In production builds, `console.log` calls are stripped via Vite's terser `pure_funcs` option as a safety net for any missed migrations.
3 changes: 2 additions & 1 deletion src/components/LazyComponents.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { lazy, Suspense } from "react";
import { logger } from "../utils/logger";
import Loading from "./common/Loading";

// Lazy load page components - Shared
Expand Down Expand Up @@ -77,7 +78,7 @@ export const LazyGasTracker = withSuspense(GasTracker);
* This ensures navigation between pages is instant (no chunk download delay).
*/
export function preloadAllRoutes() {
console.log("Preloading all route chunks...");
logger.debug("Preloading all route chunks...");
// Shared pages
import("./pages/home");
import("./pages/settings");
Expand Down
5 changes: 3 additions & 2 deletions src/components/common/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import i18next from "i18next";
import React, { Component, type ErrorInfo, type ReactNode } from "react";
import { logger } from "../../utils/logger";

interface Props {
children: ReactNode;
Expand All @@ -25,7 +26,7 @@ export class ErrorBoundary extends Component<Props, State> {

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Log error details
console.error("ErrorBoundary caught an error:", error, errorInfo);
logger.error("ErrorBoundary caught an error:", error, errorInfo);

this.setState({
error,
Expand Down Expand Up @@ -107,7 +108,7 @@ export const useErrorHandler = () => {
}, []);

const captureError = React.useCallback((error: Error) => {
console.error("Error captured:", error);
logger.error("Error captured:", error);
setError(error);
}, []);

Expand Down
5 changes: 3 additions & 2 deletions src/components/navbar/NetworkBlockIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AppContext, useNetworks } from "../../context/AppContext";
import { RpcClient } from "@openscan/network-connectors";
import { useDataService } from "../../hooks/useDataService";
import { formatGasPrice } from "../../utils/formatUtils";
import { logger } from "../../utils/logger";
import { resolveNetwork, getNetworkRpcKey } from "../../utils/networkResolver";

interface NetworkBlockIndicatorProps {
Expand Down Expand Up @@ -74,12 +75,12 @@ export function NetworkBlockIndicator({ className }: NetworkBlockIndicatorProps)
setGasPrice(gasPricesResult.data.average);
}
} catch (error) {
console.error("Failed to fetch gas price:", error);
logger.error("Failed to fetch gas price:", error);
}
}
}
} catch (error) {
console.error("Failed to fetch block number:", error);
logger.error("Failed to fetch block number:", error);
if (isMounted) {
setIsLoading(false);
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/pages/about/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type React from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { getEnabledNetworks } from "../../../config/networks";
import { logger } from "../../../utils/logger";
import NetworkIcon from "../../common/NetworkIcon";

interface GitHubStats {
Expand Down Expand Up @@ -54,7 +55,7 @@ const About: React.FC = () => {
}
}
} catch (error) {
console.error("Failed to fetch GitHub stats:", error);
logger.error("Failed to fetch GitHub stats:", error);
}
};

Expand Down
3 changes: 2 additions & 1 deletion src/components/pages/bitcoin/BitcoinBlocksPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
formatTimestamp,
truncateBlockHash,
} from "../../../utils/bitcoinFormatters";
import { logger } from "../../../utils/logger";
import Loader from "../../common/Loader";

export default function BitcoinBlocksPage() {
Expand Down Expand Up @@ -65,7 +66,7 @@ export default function BitcoinBlocksPage() {

setBlocks(fetchedBlocks);
} catch (err) {
console.error("Error fetching Bitcoin blocks:", err);
logger.error("Error fetching Bitcoin blocks:", err);
setError(err instanceof Error ? err.message : "Failed to fetch blocks");
} finally {
setLoading(false);
Expand Down
3 changes: 2 additions & 1 deletion src/components/pages/bitcoin/BitcoinMempoolPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useDataService } from "../../../hooks/useDataService";
import type { BitcoinNetworkStats, BitcoinTransaction } from "../../../types";
import { formatBTC, formatTimeAgo, truncateHash } from "../../../utils/bitcoinFormatters";
import { calculateTotalOutput } from "../../../utils/bitcoinUtils";
import { logger } from "../../../utils/logger";
import Loader from "../../common/Loader";

const PAGE_SIZE = 100;
Expand Down Expand Up @@ -48,7 +49,7 @@ export default function BitcoinMempoolPage() {
setTotalCount(mempoolResult.total);
setError(null);
} catch (err) {
console.error("Error fetching mempool data:", err);
logger.error("Error fetching mempool data:", err);
setError(err instanceof Error ? err.message : "Failed to fetch mempool data");
} finally {
setLoading(false);
Expand Down
3 changes: 2 additions & 1 deletion src/components/pages/bitcoin/BitcoinTransactionsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
truncateHash,
} from "../../../utils/bitcoinFormatters";
import { calculateTotalOutput } from "../../../utils/bitcoinUtils";
import { logger } from "../../../utils/logger";
import Loader from "../../common/Loader";

export default function BitcoinTransactionsPage() {
Expand Down Expand Up @@ -40,7 +41,7 @@ export default function BitcoinTransactionsPage() {
const txs = await adapter.getLatestTransactions(TXS_PER_PAGE);
setTransactions(txs);
} catch (err) {
console.error("Error fetching Bitcoin transactions:", err);
logger.error("Error fetching Bitcoin transactions:", err);
setError(err instanceof Error ? err.message : "Failed to fetch transactions");
} finally {
setLoading(false);
Expand Down
5 changes: 3 additions & 2 deletions src/components/pages/evm/address/AddressDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { encodeFunctionData, parseEther, toFunctionSelector } from "viem";
import { useWaitForTransactionReceipt, useWriteContract } from "wagmi";
import { AppContext } from "../../../../context";
import { useSourcify } from "../../../../hooks/useSourcify";
import { logger } from "../../../../utils/logger";
import type {
ABI,
ABIParameter,
Expand Down Expand Up @@ -242,7 +243,7 @@ const AddressDisplay: React.FC<AddressDisplayProps> = React.memo(
value: txValue,
});
} catch (err) {
console.error("Error writing to contract:", err);
logger.error("Error writing to contract:", err);
alert(`Error: ${err instanceof Error ? err.message : "Unknown error"}`);
}
}, [selectedWriteFunction, functionInputs, addressHash, contractData?.abi, writeContract]);
Expand Down Expand Up @@ -320,7 +321,7 @@ const AddressDisplay: React.FC<AddressDisplayProps> = React.memo(

setReadFunctionResult(data.result);
} catch (err) {
console.error("Error reading from contract:", err);
logger.error("Error reading from contract:", err);
setReadFunctionResult(`Error: ${err instanceof Error ? err.message : "Unknown error"}`);
} finally {
setIsReadingFunction(false);
Expand Down
5 changes: 4 additions & 1 deletion src/components/pages/evm/address/displays/ERC1155Display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "../../../../../services/MetadataService";
import type { Address, ENSReverseResult, RPCMetadata } from "../../../../../types";
import { decodeAbiString } from "../../../../../utils/hexUtils";
import { logger } from "../../../../../utils/logger";
import { AddressHeader } from "../shared";
import ContractInfoCard from "../shared/ContractInfoCard";
import ContractInfoCards from "../shared/ContractInfoCards";
Expand Down Expand Up @@ -55,7 +56,9 @@ const ERC1155Display: React.FC<ERC1155DisplayProps> = ({

// Fetch token metadata from explorer-metadata
useEffect(() => {
fetchToken(Number(networkId), addressHash).then(setTokenMetadata).catch(console.error);
fetchToken(Number(networkId), addressHash)
.then(setTokenMetadata)
.catch((err) => logger.error("Failed to fetch token metadata:", err));
}, [networkId, addressHash]);

// Fetch on-chain ERC1155 data
Expand Down
5 changes: 4 additions & 1 deletion src/components/pages/evm/address/displays/ERC20Display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "../../../../../services/MetadataService";
import type { Address, ENSReverseResult, RPCMetadata } from "../../../../../types";
import { hexToUtf8 } from "../../../../../utils/erc20Utils";
import { logger } from "../../../../../utils/logger";
import { AddressHeader } from "../shared";
import ContractInfoCard from "../shared/ContractInfoCard";
import ContractInfoCards from "../shared/ContractInfoCards";
Expand Down Expand Up @@ -56,7 +57,9 @@ const ERC20Display: React.FC<ERC20DisplayProps> = ({

// Fetch token metadata from explorer-metadata
useEffect(() => {
fetchToken(Number(networkId), addressHash).then(setTokenMetadata).catch(console.error);
fetchToken(Number(networkId), addressHash)
.then(setTokenMetadata)
.catch((err) => logger.error("Failed to fetch token metadata:", err));
}, [networkId, addressHash]);

// Fetch on-chain token data
Expand Down
5 changes: 4 additions & 1 deletion src/components/pages/evm/address/displays/ERC721Display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "../../../../../services/MetadataService";
import type { Address, ENSReverseResult, RPCMetadata } from "../../../../../types";
import { decodeAbiString } from "../../../../../utils/hexUtils";
import { logger } from "../../../../../utils/logger";
import { AddressHeader } from "../shared";
import ContractInfoCard from "../shared/ContractInfoCard";
import ContractInfoCards from "../shared/ContractInfoCards";
Expand Down Expand Up @@ -55,7 +56,9 @@ const ERC721Display: React.FC<ERC721DisplayProps> = ({

// Fetch token metadata from explorer-metadata
useEffect(() => {
fetchToken(Number(networkId), addressHash).then(setTokenMetadata).catch(console.error);
fetchToken(Number(networkId), addressHash)
.then(setTokenMetadata)
.catch((err) => logger.error("Failed to fetch token metadata:", err));
}, [networkId, addressHash]);

// Fetch on-chain NFT data
Expand Down
3 changes: 2 additions & 1 deletion src/components/pages/evm/address/shared/AccountInfoCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useContext, useEffect, useState } from "react";
import { AppContext } from "../../../../../context";
import { getNativeTokenPrice } from "../../../../../services/PriceService";
import type { Address, ENSReverseResult } from "../../../../../types";
import { logger } from "../../../../../utils/logger";
import AccountMoreInfoCard from "./AccountMoreInfoCard";
import AccountOverviewCard from "./AccountOverviewCard";

Expand Down Expand Up @@ -48,7 +49,7 @@ const AccountInfoCards: React.FC<AccountInfoCardsProps> = ({
const price = await getNativeTokenPrice(networkId, rpcUrl, mainnetRpcUrl);
setNativeTokenPrice(price);
} catch (err) {
console.error("Error fetching native token price:", err);
logger.error("Error fetching native token price:", err);
setNativeTokenPrice(null);
} finally {
setPriceLoading(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useContext, useEffect, useState } from "react";
import { AppContext } from "../../../../../context";
import { getNativeTokenPrice } from "../../../../../services/PriceService";
import type { Address, ENSReverseResult } from "../../../../../types";
import { logger } from "../../../../../utils/logger";
import AccountOverviewCard from "./AccountOverviewCard";
import ContractMoreInfoCard from "./ContractMoreInfoCard";

Expand Down Expand Up @@ -48,7 +49,7 @@ const ContractInfoCards: React.FC<ContractInfoCardsProps> = ({
const price = await getNativeTokenPrice(networkId, rpcUrl, mainnetRpcUrl);
setNativeTokenPrice(price);
} catch (err) {
console.error("Error fetching native token price:", err);
logger.error("Error fetching native token price:", err);
setNativeTokenPrice(null);
} finally {
setPriceLoading(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { ConnectButton } from "@rainbow-me/rainbowkit";
import type React from "react";
import { useCallback, useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { encodeFunctionData, parseEther } from "viem";
import { useWaitForTransactionReceipt, useWriteContract } from "wagmi";
import { AppContext } from "../../../../../context";
import type { ABI, ABIParameter, EventABI, FunctionABI } from "../../../../../types";
import { useTranslation } from "react-i18next";
import { logger } from "../../../../../utils/logger";

/**
* Generate a unique key for a function based on its name and input types
Expand Down Expand Up @@ -153,7 +154,7 @@ const ContractInteraction: React.FC<ContractInteractionProps> = ({
value: txValue,
});
} catch (err) {
console.error("Error writing to contract:", err);
logger.error("Error writing to contract:", err);
alert(`Error: ${err instanceof Error ? err.message : "Unknown error"}`);
}
}, [selectedWriteFunction, functionInputs, addressHash, abi, writeContract]);
Expand Down Expand Up @@ -224,7 +225,7 @@ const ContractInteraction: React.FC<ContractInteractionProps> = ({

setReadFunctionResult(data.result);
} catch (err) {
console.error("Error reading from contract:", err);
logger.error("Error reading from contract:", err);
setReadFunctionResult(`Error: ${err instanceof Error ? err.message : "Unknown error"}`);
} finally {
setIsReadingFunction(false);
Expand Down
5 changes: 3 additions & 2 deletions src/components/pages/evm/address/shared/CustomTokenModal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type React from "react";
import { useCallback, useContext, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { AppContext } from "../../../../../context";
import { fetchToken, getAssetUrl } from "../../../../../services/MetadataService";
import { useTranslation } from "react-i18next";
import { logger } from "../../../../../utils/logger";
import {
fetchERC20TokenInfo,
formatTokenBalance,
Expand Down Expand Up @@ -86,7 +87,7 @@ const CustomTokenModal: React.FC<CustomTokenModalProps> = ({
setTokenInfo(info);
}
} catch (err) {
console.error("Error fetching token info:", err);
logger.error("Error fetching token info:", err);
setError("Failed to fetch token information");
setTokenInfo(null);
} finally {
Expand Down
5 changes: 3 additions & 2 deletions src/components/pages/evm/address/shared/TokenHoldings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type React from "react";
import { useCallback, useContext, useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { AppContext } from "../../../../../context";
import { logger } from "../../../../../utils/logger";

interface TokenLogoProps {
src?: string;
Expand Down Expand Up @@ -176,7 +177,7 @@ const TokenHoldings: React.FC<TokenHoldingsProps> = ({
const tokenHoldings = await fetchSupporterTokenBalances(tokens, rpcUrl);
setHoldings(tokenHoldings);
} catch (err) {
console.error("Error fetching supporter tokens:", err);
logger.error("Error fetching supporter tokens:", err);
setError("Failed to fetch token holdings");
} finally {
setLoading(false);
Expand Down Expand Up @@ -231,7 +232,7 @@ const TokenHoldings: React.FC<TokenHoldingsProps> = ({

setPopularLoaded(true);
} catch (err) {
console.error("Error fetching popular tokens:", err);
logger.error("Error fetching popular tokens:", err);
setError("Failed to fetch popular tokens");
} finally {
setPopularLoading(false);
Expand Down
5 changes: 3 additions & 2 deletions src/components/pages/evm/blocks/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { RPCIndicator } from "../../../common/RPCIndicator";
import { useDataService } from "../../../../hooks/useDataService";
import { useProviderSelection } from "../../../../hooks/useProviderSelection";
import type { Block, DataWithMetadata } from "../../../../types";
import { logger } from "../../../../utils/logger";
import Loader from "../../../common/Loader";

const BLOCKS_PER_PAGE = 10;
Expand Down Expand Up @@ -89,12 +90,12 @@ export default function Blocks() {
blockNumbers.map((num) => dataService.networkAdapter.getBlock(num)),
);

console.log("Fetched blocks:", blockResults);
logger.debug("Fetched blocks:", blockResults);
// Store complete results with metadata
setBlocksResult(blockResults);
// biome-ignore lint/suspicious/noExplicitAny: <TODO>
} catch (err: any) {
console.error("Error fetching blocks:", err);
logger.error("Error fetching blocks:", err);
setError(err.message || t("errors.failedToFetchBlocks"));
} finally {
setLoading(false);
Expand Down
Loading