Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ OpenScan is a trustless, open-source, standalone web-app and multi-chain blockch

```bash
npm start
# Runs on http://localhost:3000
# Runs on http://localhost:3030
```

### Build for Production
Expand Down Expand Up @@ -89,11 +89,11 @@ OpenScan follows a layered architecture with clear separation between data fetch
- Returns `DataWithMetadata<T>` when using parallel strategy, containing:
- `data`: The primary result (from first successful provider)
- `metadata`: Optional RPCMetadata with all provider responses, hashes, and inconsistency flags
- 30-second in-memory cache keyed by `chainId:type:identifier`
- 30-second in-memory cache keyed by `networkId:type:identifier`
- Supports trace operations (debug_traceTransaction, etc.) for localhost networks only

5. **Hook Layer** (`hooks/`) - React integration
- `useDataService(chainId)`: Creates DataService instance with strategy from settings
- `useDataService(networkId)`: Creates DataService instance with strategy from settings
- `useProviderSelection`: Manages user's selected RPC provider in parallel mode
- `useSelectedData`: Extracts data from specific provider based on user selection

Expand Down Expand Up @@ -184,6 +184,7 @@ OpenScan includes special support for localhost development:
- Use `npm run lint:fix` to auto-fix linting issues (max 1024 diagnostics shown)
- **TypeScript** with strict mode (`noImplicitAny`, `noImplicitReturns`, `noUncheckedIndexedAccess`)
- **React 19** with functional components and hooks
- **CSS** All styles should be on `src/styles` folder, avoid using in line component styles.

## Coding Standards and Workflow

Expand All @@ -205,13 +206,19 @@ npm run typecheck
npm run test:run
```

### Commits

- Follow the convetional commit standard v1.0.0
- Commit without claude attribution

### Code Quality Requirements

- All code must pass Biome formatting and linting checks
- All TypeScript code must pass type checking with zero errors
- Follow the 100-character line width limit
- Use 2-space indentation consistently
- Adhere to Biome's recommended linting rules
- Keep documentation up to date.

### When Claude Code Modifies Files

Expand All @@ -238,9 +245,9 @@ npm run test:run

### When Working with Cache

- Cache keys format: `${chainId}:${type}:${identifier}`
- Cache keys format: `${networkId}:${type}:${identifier}`
- Don't cache "latest" block queries
- Clear cache when switching networks using `clearCacheForChain(chainId)`
- Clear cache when switching networks using `clearCacheForChain(networkId)`
- Default timeout: 30 seconds

### RPC Strategy Switching
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ npm install
npm start
```

The app will open at `http://localhost:3000`
The app will open at `http://localhost:3030`

### Use with Anvil/Foundry

Expand Down
29 changes: 21 additions & 8 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { darkTheme, lightTheme, RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useCallback } from "react";
import { useCallback, useEffect } from "react";
import { HashRouter, Navigate, Route, BrowserRouter as Router, Routes } from "react-router-dom";
import { WagmiProvider } from "wagmi";
import { cssVariables, getRainbowKitTheme } from "./theme";
import { networkConfig } from "./utils/networkConfig";
import "./styles/rainbowkit.css";
import "@rainbow-me/rainbowkit/styles.css";

// Detect if we're running on GitHub Pages and get the correct basename
function getBasename(): string {
Expand All @@ -28,6 +29,7 @@ import Footer from "./components/common/Footer";
import { IsometricBlocks } from "./components/common/IsometricBlocks";
import NotificationDisplay from "./components/common/NotificationDisplay";
import Navbar from "./components/navbar";
import "./styles/base.css";
import "./styles/styles.css";
import "./styles/layouts.css";
import "./styles/components.css";
Expand Down Expand Up @@ -148,11 +150,22 @@ function App() {
function RainbowKitProviderWrapper({ children }: { children: React.ReactNode }) {
const { isDarkMode } = useTheme();

return (
<RainbowKitProvider theme={isDarkMode ? darkTheme() : lightTheme()}>
{children}
</RainbowKitProvider>
);
// Inject CSS variables and set data-theme attribute
useEffect(() => {
// Inject CSS variables if not already present
const styleId = "openscan-theme-variables";
if (!document.getElementById(styleId)) {
const style = document.createElement("style");
style.id = styleId;
style.textContent = cssVariables;
document.head.appendChild(style);
}

// Set data-theme attribute for CSS variable switching
document.documentElement.setAttribute("data-theme", isDarkMode ? "dark" : "light");
}, [isDarkMode]);

return <RainbowKitProvider theme={getRainbowKitTheme(isDarkMode)}>{children}</RainbowKitProvider>;
}

export default App;
74 changes: 74 additions & 0 deletions src/components/common/ExtraDataDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type React from "react";
import { useMemo, useState } from "react";

interface ExtraDataDisplayProps {
hexData: string;
showToggle?: boolean;
}

const ExtraDataDisplay: React.FC<ExtraDataDisplayProps> = ({ hexData, showToggle = true }) => {
const [showRaw, setShowRaw] = useState(false);

const decoded = useMemo(() => {
if (!hexData || hexData === "0x") return null;

try {
// Remove 0x prefix
const hex = hexData.startsWith("0x") ? hexData.slice(2) : hexData;

// Convert hex to bytes
const bytes: number[] = [];
for (let i = 0; i < hex.length; i += 2) {
bytes.push(Number.parseInt(hex.substr(i, 2), 16));
}

// Check if all bytes are printable ASCII (32-126)
const isPrintable = bytes.every((b) => b >= 32 && b <= 126);

if (isPrintable && bytes.length > 0) {
return String.fromCharCode(...bytes);
}

// Try UTF-8 decoding
const decoder = new TextDecoder("utf-8", { fatal: true });
const uint8Array = new Uint8Array(bytes);
const decoded = decoder.decode(uint8Array);

// Check if result contains mostly printable characters
const printableCount = decoded.split("").filter((c) => c.charCodeAt(0) >= 32).length;
if (printableCount / decoded.length > 0.8) {
return decoded;
}

return null;
} catch {
return null;
}
}, [hexData]);

const hasDecodedValue = decoded !== null && decoded.length > 0;
const displayValue = showRaw || !hasDecodedValue ? hexData : decoded;

return (
<div className="extra-data-display">
<span className={`extra-data-value ${showRaw ? "tx-mono" : ""}`}>
{hasDecodedValue && !showRaw && (
<span className="extra-data-decoded">"{displayValue}"</span>
)}
{(!hasDecodedValue || showRaw) && <span className="tx-mono">{displayValue}</span>}
</span>
{showToggle && hasDecodedValue && (
<button
type="button"
className="extra-data-toggle"
onClick={() => setShowRaw(!showRaw)}
title={showRaw ? "Show decoded" : "Show raw hex"}
>
{showRaw ? "UTF-8" : "Hex"}
</button>
)}
</div>
);
};

export default ExtraDataDisplay;
14 changes: 11 additions & 3 deletions src/components/common/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,31 @@ const Footer: React.FC<FooterProps> = ({ className = "" }) => {
<footer className={`app-footer ${className}`}>
<div className="footer-content">
<div className="footer-left">
<button
type="button"
onClick={() => navigate("/about")}
className="footer-link btn-reset"
>
About Openscan
</button>
|
<button type="button" onClick={goToSubscriptions} className="footer-link btn-reset">
Subscribe
</button>
|
<button type="button" onClick={goToSupporters} className="footer-link btn-reset">
Our Supporters ❤️
</button>
</div>
<div className={`footer-version ${getVersionClass()}`}>
<span className="version-label">Version:</span>
<div className={`footer-right ${getVersionClass()}`}>
<a
href={`${repoUrl}/commit/${commitHash}`}
target="_blank"
rel="noopener noreferrer"
className="commit-link"
title={`View commit ${formattedCommitHash} on GitHub`}
>
{appVersion} {formattedCommitHash}
v{appVersion} {formattedCommitHash}
</a>
{ENVIRONMENT !== "production" && (
<span className="commit-link environment-tag">{ENVIRONMENT}</span>
Expand Down
125 changes: 72 additions & 53 deletions src/components/navbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useTheme } from "../../context/SettingsContext";
import { useSearch } from "../../hooks/useSearch";
import { NetworkBlockIndicator } from "./NetworkBlockIndicator";
import VersionWarningIcon from "./VersionWarningIcon";
Expand All @@ -7,6 +8,7 @@ const Navbar = () => {
const navigate = useNavigate();
const location = useLocation();
const { searchTerm, setSearchTerm, isResolving, handleSearch, networkId } = useSearch();
const { isDarkMode, toggleTheme } = useTheme();

// Check if we should show the search box (on blocks, block, txs, tx pages)
const pathSegments = location.pathname.split("/").filter(Boolean);
Expand Down Expand Up @@ -128,61 +130,78 @@ const Navbar = () => {
<li>
{/* biome-ignore lint/a11y/useButtonType: nav buttons don't submit forms */}
<button
onClick={() => navigate("/subscriptions")}
onClick={toggleTheme}
className="navbar-toggle-btn"
aria-label="Subscriptions"
title="Subscriptions"
aria-label={isDarkMode ? "Switch to light mode" : "Switch to dark mode"}
title={isDarkMode ? "Switch to light mode" : "Switch to dark mode"}
>
{/* biome-ignore lint/a11y/noSvgWithoutTitle: aria-label on button provides accessible name */}
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<polygon
points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</li>
<li>
{/** biome-ignore lint/a11y/useButtonType: <TODO> */}
<button
onClick={() => navigate("/about")}
className="navbar-toggle-btn"
aria-label="About"
title="About"
>
{/** biome-ignore lint/a11y/noSvgWithoutTitle: <TODO> */}
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" />
<path
d="M12 16v-4"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M12 8h.01"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
{isDarkMode ? (
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<circle cx="12" cy="12" r="5" stroke="currentColor" strokeWidth="2" />
<path d="M12 1v2" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
<path
d="M12 21v2"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M4.22 4.22l1.42 1.42"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M18.36 18.36l1.42 1.42"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<path d="M1 12h2" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
<path
d="M21 12h2"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M4.22 19.78l1.42-1.42"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M18.36 5.64l1.42-1.42"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
) : (
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</button>
</li>
<li>
Expand Down
Loading