Skip to content
Open
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
72 changes: 66 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

An explorer tool to look up information about cross-chain messages in [Base Bridge](https://github.com/base/bridge). Supports both testnet and mainnet.

## Features

- 🔍 **Transaction Lookup**: Search for bridge transactions using Solana signatures or Base transaction hashes
- 🔗 **Cross-Chain Tracking**: Track message flow from initiation → validation → execution
- ⏱️ **Real-Time Status**: View current status of bridge transactions (Pending, Pre-validated, Executed)
- 🌐 **Multi-Network Support**: Works with both mainnet (Base + Solana) and testnet (Base Sepolia + Solana Devnet)
- 📋 **Easy Copy**: One-click copy for transaction hashes and addresses
- 🔗 **Explorer Links**: Direct links to block explorers (Basescan, Solana Explorer)

## Quick Start

1. Install dependencies
Expand All @@ -14,12 +23,63 @@ npm install

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

3. Visit [localhost:3000](http://localhost:3000/) in your browser

## Environment Variables (Optional)

Create a `.env.local` file with custom RPC endpoints for better performance:

```env
BASE_MAINNET_RPC=https://your-base-mainnet-rpc.com
BASE_SEPOLIA_RPC=https://your-base-sepolia-rpc.com
SOLANA_MAINNET_RPC=https://your-solana-mainnet-rpc.com
SOLANA_DEVNET_RPC=https://your-solana-devnet-rpc.com
```

## Tech Stack

- **Framework**: [Next.js 15](https://nextjs.org/) with App Router
- **Styling**: [Tailwind CSS 4](https://tailwindcss.com/)
- **EVM**: [viem](https://viem.sh/) for Base blockchain interactions
- **Solana**: [@solana/web3.js](https://solana-labs.github.io/solana-web3.js/) for Solana blockchain interactions
- **TypeScript**: Full type safety throughout the codebase

## Project Structure

```
src/
├── app/ # Next.js App Router pages and API routes
│ ├── api/ # API endpoints for blockchain queries
│ ├── layout.tsx # Root layout with fonts and analytics
│ └── page.tsx # Main explorer page
├── components/ # React components
│ ├── ErrorMessage.tsx # Error display component
│ ├── ExploreButton.tsx # Main explore action button
│ ├── Footer.tsx # Footer with useful links
│ ├── Header.tsx # Page header
│ ├── InputForm.tsx # Transaction input form
│ ├── LoadingSkeleton.tsx # Loading state skeleton
│ ├── Results.tsx # Transaction results display
│ ├── Status.tsx # Status badge component
│ └── Toast.tsx # Toast notification system
└── lib/ # Utility libraries
├── base.ts # Base blockchain decoder
├── bridge.ts # Bridge types and interfaces
├── solana.ts # Solana blockchain decoder
└── transaction.ts # Transaction types
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

MIT License

## Related Projects

- [Base Bridge](https://github.com/base/bridge) - The core bridge implementation
- [Base Docs](https://docs.base.org) - Official Base documentation
37 changes: 37 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,40 @@ a:hover {
0 1px 2px rgba(15, 23, 42, 0.08), 0 8px 24px rgba(0, 0, 0, 0.12);
backdrop-filter: blur(8px);
}

/* Toast slide-in animation */
@keyframes slide-in {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}

.animate-slide-in {
animation: slide-in 0.3s ease-out;
}

/* Skeleton pulse animation override for better visibility */
@keyframes skeleton-pulse {
0%, 100% {
opacity: 0.4;
}
50% {
opacity: 0.8;
}
}

/* Focus visible improvements for accessibility */
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}

/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
36 changes: 33 additions & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,49 @@ import { BridgeQueryResult } from "@/lib/bridge";
import { Header } from "@/components/Header";
import { InputForm } from "@/components/InputForm";
import { Results } from "@/components/Results";
import { Footer } from "@/components/Footer";
import { LoadingSkeleton } from "@/components/LoadingSkeleton";
import { ErrorMessage } from "@/components/ErrorMessage";

export default function Home() {
const [result, setResult] = useState<BridgeQueryResult | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleSetResult = (r: BridgeQueryResult | null) => {
setError(null);
setResult(r);
};

const handleError = (message: string) => {
setError(message);
setResult(null);
};

return (
<div className="min-h-screen flex items-center justify-center px-8 py-20">
<div className="min-h-screen flex items-center justify-center px-8 py-20 pb-24">
<main className="relative w-full max-w-3xl md:max-w-4xl">
<Header />

<InputForm setResult={setResult} />
<InputForm
setResult={handleSetResult}
setIsLoading={setIsLoading}
setError={handleError}
/>

{error && (
<ErrorMessage
title="Transaction Not Found"
message={error}
onDismiss={() => setError(null)}
/>
)}

{isLoading && !result && <LoadingSkeleton />}

<Results result={result} setResult={setResult} />
{!isLoading && <Results result={result} setResult={handleSetResult} />}
</main>
<Footer />
</div>
);
}
73 changes: 73 additions & 0 deletions src/components/ErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"use client";

import { useState } from "react";

interface ErrorMessageProps {
title?: string;
message: string;
onDismiss?: () => void;
}

export const ErrorMessage = ({
title = "Error",
message,
onDismiss,
}: ErrorMessageProps) => {
const [isVisible, setIsVisible] = useState(true);

const handleDismiss = () => {
setIsVisible(false);
onDismiss?.();
};

if (!isVisible) return null;

return (
<div
role="alert"
className="mt-6 rounded-lg border border-red-500/20 bg-red-500/10 p-4 md:p-5"
>
<div className="flex items-start gap-3">
<div className="flex-shrink-0">
<svg
className="h-5 w-5 text-red-500"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="flex-1">
<h3 className="text-sm font-semibold text-red-600 dark:text-red-400">
{title}
</h3>
<p className="mt-1 text-sm text-red-600/80 dark:text-red-400/80">
{message}
</p>
</div>
{onDismiss && (
<button
type="button"
onClick={handleDismiss}
className="flex-shrink-0 rounded p-1 text-red-500 hover:bg-red-500/10 transition-colors"
aria-label="Dismiss error"
>
<svg
className="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
</svg>
</button>
)}
</div>
</div>
);
};
67 changes: 67 additions & 0 deletions src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
export const Footer = () => {
return (
<footer className="fixed bottom-0 left-0 right-0 border-t border-white/10 bg-white/60 dark:bg-white/5 backdrop-blur-sm">
<div className="max-w-4xl mx-auto px-4 py-4">
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 text-sm text-[var(--color-muted-foreground)]">
<div className="flex items-center gap-2">
<span
className="h-2 w-2 rounded-full"
style={{ background: "var(--brand)" }}
/>
<span>Base Bridge Explorer</span>
</div>
<div className="flex items-center gap-4">
<a
href="https://github.com/base/bridge"
target="_blank"
rel="noopener noreferrer"
className="hover:text-[var(--brand)] transition-colors flex items-center gap-1.5"
>
<svg
className="h-4 w-4"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
<span>Base Bridge</span>
</a>
<a
href="https://docs.base.org"
target="_blank"
rel="noopener noreferrer"
className="hover:text-[var(--brand)] transition-colors flex items-center gap-1.5"
>
<svg
className="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<polyline points="10 9 9 9 8 9" />
</svg>
<span>Docs</span>
</a>
<a
href="https://base.org"
target="_blank"
rel="noopener noreferrer"
className="hover:text-[var(--brand)] transition-colors"
>
Base.org
</a>
</div>
</div>
</div>
</footer>
);
};
Loading