Skip to content

crypto seeker #90

Description

@justobi

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}


)}

);
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions