import { useState, useCallback, useEffect } from "react";
import {
Search, Copy, ExternalLink, RefreshCw, Wallet,
ArrowUpRight, ArrowDownLeft, AlertCircle, CheckCircle2,
TrendingUp, Activity, Layers, ChevronRight
} from "lucide-react";
type Chain = "btc" | "eth";
interface TxRef {
tx_hash: string;
confirmed: string;
value: number;
tx_input_n: number;
tx_output_n: number;
}
interface WalletData {
address: string;
chain: Chain;
balance: number;
balanceCrypto: string;
balanceUSD: string;
totalReceived: number;
totalReceivedCrypto: string;
totalSent: number;
totalSentCrypto: string;
txCount: number;
txrefs: TxRef[];
priceUSD: number;
}
interface Prices {
btc: number;
eth: number;
}
const EXAMPLE_ADDRESSES: Record<Chain, { label: string; address: string }> = {
btc: { label: "Satoshi Genesis Block", address: "1A1zP1eP5QGefi2DMPTfTL5SLmv7Divfna" },
eth: { label: "Vitalik Buterin", address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" },
};
function detectChain(address: string): Chain | null {
const trimmed = address.trim();
if (/^0x[a-fA-F0-9]{40}$/.test(trimmed)) return "eth";
if (/^(1|3)[a-km-zA-HJ-NP-Z1-9]{25,34}$/.test(trimmed)) return "btc";
if (/^bc1[a-z0-9]{39,59}$/.test(trimmed)) return "btc";
return null;
}
function formatCrypto(value: number, chain: Chain): string {
if (chain === "btc") {
return (value / 1e8).toFixed(8) + " BTC";
}
return (value / 1e18).toFixed(6) + " ETH";
}
function formatUSD(value: number): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
}
function truncateHash(hash: string, chars = 8): string {
if (hash.length <= chars * 2 + 3) return hash;
return hash.slice(0, chars) + "..." + hash.slice(-chars);
}
function timeAgo(dateStr: string): string {
if (!dateStr) return "Pending";
const date = new Date(dateStr);
const now = Date.now();
const diff = Math.floor((now - date.getTime()) / 1000);
if (diff < 60) return ${diff}s ago;
if (diff < 3600) return ${Math.floor(diff / 60)}m ago;
if (diff < 86400) return ${Math.floor(diff / 3600)}h ago;
if (diff < 2592000) return ${Math.floor(diff / 86400)}d ago;
return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
}
function chainExplorerUrl(chain: Chain, address: string): string {
if (chain === "btc") return https://blockchair.com/bitcoin/address/${address};
return https://etherscan.io/address/${address};
}
function txExplorerUrl(chain: Chain, hash: string): string {
if (chain === "btc") return https://blockchair.com/bitcoin/transaction/${hash};
return https://etherscan.io/tx/${hash};
}
export default function App() {
const [query, setQuery] = useState("");
const [selectedChain, setSelectedChain] = useState("eth");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [wallet, setWallet] = useState<WalletData | null>(null);
const [copied, setCopied] = useState(false);
const [prices, setPrices] = useState({ btc: 0, eth: 0 });
useEffect(() => {
fetch("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum&vs_currencies=usd")
.then((r) => r.json())
.then((data) => {
setPrices({ btc: data.bitcoin?.usd ?? 0, eth: data.ethereum?.usd ?? 0 });
})
.catch(() => {});
}, []);
const search = useCallback(async (address: string, chain: Chain) => {
const trimmed = address.trim();
if (!trimmed) return;
setLoading(true);
setError(null);
setWallet(null);
try {
const chainSlug = chain === "btc" ? "btc/main" : "eth/main";
const url = `https://api.blockcypher.com/v1/${chainSlug}/addrs/${trimmed}?limit=10&omitWalletAddresses=true`;
const res = await fetch(url);
if (!res.ok) {
if (res.status === 404) throw new Error("Address not found on chain.");
if (res.status === 429) throw new Error("Rate limited. Please wait a moment and try again.");
throw new Error(`API error: ${res.status}`);
}
const data = await res.json();
const price = chain === "btc" ? prices.btc : prices.eth;
const divisor = chain === "btc" ? 1e8 : 1e18;
const balanceNative = (data.balance ?? 0) / divisor;
const receivedNative = (data.total_received ?? 0) / divisor;
const sentNative = (data.total_sent ?? 0) / divisor;
setWallet({
address: trimmed,
chain,
balance: data.balance ?? 0,
balanceCrypto: formatCrypto(data.balance ?? 0, chain),
balanceUSD: price > 0 ? formatUSD(balanceNative * price) : "—",
totalReceived: data.total_received ?? 0,
totalReceivedCrypto: formatCrypto(data.total_received ?? 0, chain),
totalSent: data.total_sent ?? 0,
totalSentCrypto: formatCrypto(data.total_sent ?? 0, chain),
txCount: data.n_tx ?? 0,
txrefs: data.txrefs ?? [],
priceUSD: price,
});
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error occurred.");
} finally {
setLoading(false);
}
}, [prices]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const detected = detectChain(query);
const chain = detected ?? selectedChain;
if (detected) setSelectedChain(detected);
search(query, chain);
};
const handleExample = (chain: Chain) => {
const ex = EXAMPLE_ADDRESSES[chain];
setQuery(ex.address);
setSelectedChain(chain);
search(ex.address, chain);
};
const copyAddress = () => {
if (!wallet) return;
navigator.clipboard.writeText(wallet.address);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div
className="min-h-screen w-full bg-background text-foreground"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
{/* Grid overlay */}
<div
className="pointer-events-none fixed inset-0 opacity-[0.025]"
style={{
backgroundImage:
"linear-gradient(rgba(6,255,165,0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(6,255,165,0.5) 1px, transparent 1px)",
backgroundSize: "40px 40px",
}}
/>
<div className="relative z-10 max-w-4xl mx-auto px-4 py-12 md:py-20">
{/* Header */}
<div className="mb-12">
<div className="flex items-center gap-2 mb-3">
<div className="w-2 h-2 rounded-full bg-primary animate-pulse" />
<span
className="text-xs tracking-[0.2em] text-muted-foreground uppercase"
style={{ fontFamily: "'Geist Mono', monospace" }}
>
Blockchain Explorer
</span>
</div>
<h1
className="text-5xl md:text-7xl font-bold text-foreground leading-none tracking-tight mb-4"
style={{ fontFamily: "'Barlow Condensed', sans-serif", fontWeight: 700 }}
>
WALLET
<br />
<span className="text-primary">SEARCHER</span>
</h1>
<p className="text-muted-foreground text-sm max-w-md">
Look up any Bitcoin or Ethereum wallet. Paste an address below — chain auto-detected.
</p>
</div>
{/* Chain selector + search */}
<form onSubmit={handleSubmit} className="mb-8">
{/* Chain pills */}
<div className="flex gap-2 mb-3">
{(["eth", "btc"] as Chain[]).map((c) => (
<button
key={c}
type="button"
onClick={() => setSelectedChain(c)}
className={`px-4 py-1.5 rounded text-xs tracking-widest uppercase transition-all border ${
selectedChain === c
? "bg-primary text-primary-foreground border-primary"
: "border-border text-muted-foreground hover:border-primary/40 hover:text-foreground"
}`}
style={{ fontFamily: "'Geist Mono', monospace" }}
>
{c === "eth" ? "Ethereum" : "Bitcoin"}
</button>
))}
</div>
{/* Search input */}
<div className="relative flex gap-2">
<div className="relative flex-1">
<Search
size={16}
className="absolute left-4 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={
selectedChain === "eth"
? "0x742d35Cc6634C0532925a3b8D4C9D5..."
: "1A1zP1eP5QGefi2DMPTfTL5SLmv7D..."
}
className="w-full pl-10 pr-4 py-3.5 rounded bg-card border border-border text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:border-primary/60 focus:ring-1 focus:ring-primary/30 transition-all text-sm"
style={{ fontFamily: "'Geist Mono', monospace", fontSize: "0.8rem" }}
spellCheck={false}
/>
</div>
<button
type="submit"
disabled={!query.trim() || loading}
className="px-6 py-3.5 bg-primary text-primary-foreground font-semibold rounded hover:bg-primary/90 disabled:opacity-40 disabled:cursor-not-allowed transition-all flex items-center gap-2 text-sm whitespace-nowrap"
style={{ fontFamily: "'Barlow Condensed', sans-serif", letterSpacing: "0.05em" }}
>
{loading ? (
<RefreshCw size={15} className="animate-spin" />
) : (
<Search size={15} />
)}
SEARCH
</button>
</div>
{/* Example shortcuts */}
<div className="flex gap-4 mt-3">
<span className="text-xs text-muted-foreground">Try:</span>
{(["eth", "btc"] as Chain[]).map((c) => (
<button
key={c}
type="button"
onClick={() => handleExample(c)}
className="text-xs text-primary/70 hover:text-primary transition-colors underline underline-offset-2 decoration-primary/30"
style={{ fontFamily: "'Geist Mono', monospace" }}
>
{EXAMPLE_ADDRESSES[c].label}
</button>
))}
</div>
</form>
{/* Error */}
{error && (
<div className="flex items-start gap-3 p-4 rounded border border-destructive/30 bg-destructive/5 mb-6">
<AlertCircle size={16} className="text-destructive mt-0.5 shrink-0" />
<div>
<p className="text-sm text-destructive font-medium">Search failed</p>
<p className="text-xs text-muted-foreground mt-0.5">{error}</p>
</div>
</div>
)}
{/* Loading skeleton */}
{loading && (
<div className="space-y-4">
<div className="h-32 rounded border border-border bg-card animate-pulse" />
<div className="grid grid-cols-3 gap-3">
{[0, 1, 2].map((i) => (
<div key={i} className="h-20 rounded border border-border bg-card animate-pulse" />
))}
</div>
</div>
)}
{/* Wallet result */}
{wallet && !loading && (
<div className="space-y-4">
{/* Address card */}
<div className="p-5 rounded border border-border bg-card">
<div className="flex items-start justify-between gap-4 flex-wrap">
<div className="min-w-0">
<div className="flex items-center gap-2 mb-1">
<Wallet size={14} className="text-primary shrink-0" />
<span
className="text-xs text-muted-foreground tracking-widest uppercase"
style={{ fontFamily: "'Geist Mono', monospace" }}
>
{wallet.chain === "eth" ? "Ethereum Wallet" : "Bitcoin Wallet"}
</span>
</div>
<p
className="text-sm text-foreground break-all"
style={{ fontFamily: "'Geist Mono', monospace" }}
>
{wallet.address}
</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<button
onClick={copyAddress}
className="flex items-center gap-1.5 px-3 py-1.5 rounded border border-border text-xs text-muted-foreground hover:text-foreground hover:border-primary/40 transition-all"
style={{ fontFamily: "'Geist Mono', monospace" }}
>
{copied ? (
<CheckCircle2 size={13} className="text-primary" />
) : (
<Copy size={13} />
)}
{copied ? "Copied" : "Copy"}
</button>
<a
href={chainExplorerUrl(wallet.chain, wallet.address)}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 px-3 py-1.5 rounded border border-border text-xs text-muted-foreground hover:text-foreground hover:border-primary/40 transition-all"
style={{ fontFamily: "'Geist Mono', monospace" }}
>
<ExternalLink size={13} />
Explorer
</a>
</div>
</div>
</div>
{/* Balance hero */}
<div className="p-6 rounded border border-primary/20 bg-card relative overflow-hidden">
<div
className="absolute inset-0 opacity-[0.03]"
style={{
background:
"radial-gradient(ellipse at 0% 50%, #06ffa5 0%, transparent 70%)",
}}
/>
<p className="text-xs text-muted-foreground tracking-widest uppercase mb-2"
style={{ fontFamily: "'Geist Mono', monospace" }}>
Current Balance
</p>
<div className="flex items-end gap-4 flex-wrap">
<p
className="text-4xl md:text-5xl font-bold text-primary leading-none"
style={{ fontFamily: "'Barlow Condensed', sans-serif" }}
>
{wallet.balanceCrypto}
</p>
{wallet.priceUSD > 0 && (
<p className="text-xl text-muted-foreground mb-0.5">{wallet.balanceUSD}</p>
)}
</div>
{wallet.priceUSD > 0 && (
<p className="text-xs text-muted-foreground mt-2"
style={{ fontFamily: "'Geist Mono', monospace" }}>
@ {formatUSD(wallet.priceUSD)} /{wallet.chain.toUpperCase()}
</p>
)}
</div>
{/* Stats row */}
<div className="grid grid-cols-3 gap-3">
<StatCard
icon={<ArrowDownLeft size={14} className="text-emerald-400" />}
label="Total Received"
value={wallet.totalReceivedCrypto}
sub={wallet.priceUSD > 0 ? formatUSD((wallet.totalReceived / (wallet.chain === "btc" ? 1e8 : 1e18)) * wallet.priceUSD) : undefined}
/>
<StatCard
icon={<ArrowUpRight size={14} className="text-red-400" />}
label="Total Sent"
value={wallet.totalSentCrypto}
sub={wallet.priceUSD > 0 ? formatUSD((wallet.totalSent / (wallet.chain === "btc" ? 1e8 : 1e18)) * wallet.priceUSD) : undefined}
/>
<StatCard
icon={<Activity size={14} className="text-accent" />}
label="Transactions"
value={wallet.txCount.toLocaleString()}
sub="total on-chain"
/>
</div>
{/* Transactions */}
{wallet.txrefs.length > 0 && (
<div className="rounded border border-border bg-card overflow-hidden">
<div className="flex items-center gap-2 px-5 py-3 border-b border-border">
<Layers size={14} className="text-muted-foreground" />
<span
className="text-xs text-muted-foreground tracking-widest uppercase"
style={{ fontFamily: "'Geist Mono', monospace" }}
>
Recent Transactions
</span>
<span className="ml-auto text-xs text-muted-foreground/50"
style={{ fontFamily: "'Geist Mono', monospace" }}>
last {wallet.txrefs.length}
</span>
</div>
<div className="divide-y divide-border">
{wallet.txrefs.map((tx, i) => {
const isIn = tx.tx_input_n === -1;
const native = tx.value / (wallet.chain === "btc" ? 1e8 : 1e18);
const symbol = wallet.chain === "btc" ? "BTC" : "ETH";
return (
<div
key={`${tx.tx_hash}-${i}`}
className="flex items-center gap-3 px-5 py-3.5 hover:bg-secondary/30 transition-colors"
>
<div className={`w-7 h-7 rounded flex items-center justify-center shrink-0 ${isIn ? "bg-emerald-500/10" : "bg-red-500/10"}`}>
{isIn ? (
<ArrowDownLeft size={13} className="text-emerald-400" />
) : (
<ArrowUpRight size={13} className="text-red-400" />
)}
</div>
<div className="min-w-0 flex-1">
<a
href={txExplorerUrl(wallet.chain, tx.tx_hash)}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-foreground/70 hover:text-primary transition-colors flex items-center gap-1"
style={{ fontFamily: "'Geist Mono', monospace" }}
>
{truncateHash(tx.tx_hash)}
<ChevronRight size={11} />
</a>
<p className="text-xs text-muted-foreground mt-0.5">
{timeAgo(tx.confirmed)}
</p>
</div>
<div className="text-right shrink-0">
<p
className={`text-sm font-medium ${isIn ? "text-emerald-400" : "text-red-400"}`}
style={{ fontFamily: "'Geist Mono', monospace" }}
>
{isIn ? "+" : "-"}{native.toFixed(wallet.chain === "btc" ? 6 : 4)} {symbol}
</p>
{wallet.priceUSD > 0 && (
<p className="text-xs text-muted-foreground">
{formatUSD(native * wallet.priceUSD)}
</p>
)}
</div>
</div>
);
})}
</div>
</div>
)}
{wallet.txrefs.length === 0 && wallet.txCount > 0 && (
<div className="p-5 rounded border border-border bg-card text-center">
<p className="text-xs text-muted-foreground" style={{ fontFamily: "'Geist Mono', monospace" }}>
{wallet.txCount} transactions found — detailed history unavailable from public API.
</p>
</div>
)}
</div>
)}
{/* Empty state */}
{!wallet && !loading && !error && (
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="w-16 h-16 rounded-full border border-primary/20 flex items-center justify-center mb-4">
<Search size={24} className="text-primary/40" />
</div>
<p className="text-muted-foreground text-sm mb-1">Enter a wallet address above</p>
<p className="text-muted-foreground/50 text-xs" style={{ fontFamily: "'Geist Mono', monospace" }}>
BTC and ETH supported · live data via BlockCypher
</p>
</div>
)}
{/* Footer */}
<div className="mt-16 pt-6 border-t border-border flex items-center justify-between flex-wrap gap-2">
<p className="text-xs text-muted-foreground/40" style={{ fontFamily: "'Geist Mono', monospace" }}>
Data: BlockCypher API · Prices: CoinGecko
</p>
<div className="flex items-center gap-1.5">
<div className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />
<span className="text-xs text-muted-foreground/40" style={{ fontFamily: "'Geist Mono', monospace" }}>
live
</span>
</div>
</div>
</div>
</div>
);
}
function StatCard({
icon,
label,
value,
sub,
}: {
icon: React.ReactNode;
label: string;
value: string;
sub?: string;
}) {
return (
{icon}
<span
className="text-xs text-muted-foreground tracking-wider uppercase"
style={{ fontFamily: "'Geist Mono', monospace", fontSize: "0.65rem" }}
>
{label}
<p
className="text-sm font-medium text-foreground leading-tight"
style={{ fontFamily: "'Geist Mono', monospace" }}
>
{value}
{sub && (
<p className="text-xs text-muted-foreground/60 mt-0.5" style={{ fontFamily: "'Geist Mono', monospace" }}>
{sub}
)}
);
}
import { useState, useCallback, useEffect } from "react";
import {
Search, Copy, ExternalLink, RefreshCw, Wallet,
ArrowUpRight, ArrowDownLeft, AlertCircle, CheckCircle2,
TrendingUp, Activity, Layers, ChevronRight
} from "lucide-react";
type Chain = "btc" | "eth";
interface TxRef {
tx_hash: string;
confirmed: string;
value: number;
tx_input_n: number;
tx_output_n: number;
}
interface WalletData {
address: string;
chain: Chain;
balance: number;
balanceCrypto: string;
balanceUSD: string;
totalReceived: number;
totalReceivedCrypto: string;
totalSent: number;
totalSentCrypto: string;
txCount: number;
txrefs: TxRef[];
priceUSD: number;
}
interface Prices {
btc: number;
eth: number;
}
const EXAMPLE_ADDRESSES: Record<Chain, { label: string; address: string }> = {
btc: { label: "Satoshi Genesis Block", address: "1A1zP1eP5QGefi2DMPTfTL5SLmv7Divfna" },
eth: { label: "Vitalik Buterin", address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" },
};
function detectChain(address: string): Chain | null {
const trimmed = address.trim();
if (/^0x[a-fA-F0-9]{40}$/.test(trimmed)) return "eth";
if (/^(1|3)[a-km-zA-HJ-NP-Z1-9]{25,34}$/.test(trimmed)) return "btc";
if (/^bc1[a-z0-9]{39,59}$/.test(trimmed)) return "btc";
return null;
}
function formatCrypto(value: number, chain: Chain): string {
if (chain === "btc") {
return (value / 1e8).toFixed(8) + " BTC";
}
return (value / 1e18).toFixed(6) + " ETH";
}
function formatUSD(value: number): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
}
function truncateHash(hash: string, chars = 8): string {
if (hash.length <= chars * 2 + 3) return hash;
return hash.slice(0, chars) + "..." + hash.slice(-chars);
}
function timeAgo(dateStr: string): string {
if (!dateStr) return "Pending";
const date = new Date(dateStr);
const now = Date.now();
const diff = Math.floor((now - date.getTime()) / 1000);
if (diff < 60) return
${diff}s ago;if (diff < 3600) return
${Math.floor(diff / 60)}m ago;if (diff < 86400) return
${Math.floor(diff / 3600)}h ago;if (diff < 2592000) return
${Math.floor(diff / 86400)}d ago;return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
}
function chainExplorerUrl(chain: Chain, address: string): string {
if (chain === "btc") return
https://blockchair.com/bitcoin/address/${address};return
https://etherscan.io/address/${address};}
function txExplorerUrl(chain: Chain, hash: string): string {
if (chain === "btc") return
https://blockchair.com/bitcoin/transaction/${hash};return
https://etherscan.io/tx/${hash};}
export default function App() {
const [query, setQuery] = useState("");
const [selectedChain, setSelectedChain] = useState("eth");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [wallet, setWallet] = useState<WalletData | null>(null);
const [copied, setCopied] = useState(false);
const [prices, setPrices] = useState({ btc: 0, eth: 0 });
useEffect(() => {
fetch("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum&vs_currencies=usd")
.then((r) => r.json())
.then((data) => {
setPrices({ btc: data.bitcoin?.usd ?? 0, eth: data.ethereum?.usd ?? 0 });
})
.catch(() => {});
}, []);
const search = useCallback(async (address: string, chain: Chain) => {
const trimmed = address.trim();
if (!trimmed) return;
setLoading(true);
setError(null);
setWallet(null);
}, [prices]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const detected = detectChain(query);
const chain = detected ?? selectedChain;
if (detected) setSelectedChain(detected);
search(query, chain);
};
const handleExample = (chain: Chain) => {
const ex = EXAMPLE_ADDRESSES[chain];
setQuery(ex.address);
setSelectedChain(chain);
search(ex.address, chain);
};
const copyAddress = () => {
if (!wallet) return;
navigator.clipboard.writeText(wallet.address);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div
className="min-h-screen w-full bg-background text-foreground"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
{/* Grid overlay */}
<div
className="pointer-events-none fixed inset-0 opacity-[0.025]"
style={{
backgroundImage:
"linear-gradient(rgba(6,255,165,0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(6,255,165,0.5) 1px, transparent 1px)",
backgroundSize: "40px 40px",
}}
/>
);
}
function StatCard({
icon,
label,
value,
sub,
}: {
icon: React.ReactNode;
label: string;
value: string;
sub?: string;
}) {
return (
{icon}
<span
className="text-xs text-muted-foreground tracking-wider uppercase"
style={{ fontFamily: "'Geist Mono', monospace", fontSize: "0.65rem" }}
>
{label}
<p
className="text-sm font-medium text-foreground leading-tight"
style={{ fontFamily: "'Geist Mono', monospace" }}
>
{value}
{sub && (
<p className="text-xs text-muted-foreground/60 mt-0.5" style={{ fontFamily: "'Geist Mono', monospace" }}>
{sub}
)}
);
}