Skip to content

crypto seeker #90

@justobi

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions