From 348e55b63e887cada9bd7505b53d23d5672828c4 Mon Sep 17 00:00:00 2001 From: drjones Date: Thu, 16 Apr 2026 01:03:14 -0700 Subject: [PATCH] Add proper deposit desk: multi-coin, clear custody model, DepositWidget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /account/add-funds: full rebuild as Deposit Desk - 3-step how-it-works explainer: you send crypto → we hold it → you shop with USD credit → we pay vendors with your crypto - Coin selector: BTC (auto-verify), ETH (manual review), XMR (manual review) - Each coin shows deposit address from env var with copy button - BTC: immediate on-chain verify via /api/btc/verify (mempool.space) - ETH/XMR: submit txhash to /api/pending-deposit for manual review - Pending deposit history stored in localStorage with status - Important notes block explaining custody, no-withdrawal, confirmations - Back-links to checkout, wallets, dashboard - /api/pending-deposit: new route to log ETH/XMR manual review requests (logs to console, ready to wire to DB/webhook) - DepositWidget component: reusable compact + full variants - compact: balance + "Deposit crypto →" CTA (used in checkout) - full: balance + how-it-works summary (used in dashboard) - Dashboard: replace balance section with DepositWidget + stats - Checkout: add compact DepositWidget above CheckoutFlow, fix back link from "Sanctuary" → "Market", update feature cards to be accurate - .env.example: document all three coin address env vars + optional payment processor URL - .env.example: add NEXT_PUBLIC_ETH_ADDRESS, NEXT_PUBLIC_XMR_ADDRESS Made-with: Cursor --- app/account/add-funds/page.tsx | 514 +++++++++++++++++++++++++------ app/api/pending-deposit/route.ts | 44 +++ app/checkout/page.tsx | 57 ++-- app/dashboard/page.tsx | 39 +-- components/DepositWidget.tsx | 85 +++++ 5 files changed, 596 insertions(+), 143 deletions(-) create mode 100644 app/api/pending-deposit/route.ts create mode 100644 components/DepositWidget.tsx diff --git a/app/account/add-funds/page.tsx b/app/account/add-funds/page.tsx index d0594ea..0b8d2d0 100644 --- a/app/account/add-funds/page.tsx +++ b/app/account/add-funds/page.tsx @@ -8,32 +8,103 @@ import { useAccount } from "@/contexts/AccountContext"; import { useWallet } from "@/contexts/WalletContext"; import { getMerchantBtcAddress, isMerchantBtcConfigured } from "@/lib/merchantBtc"; +type Coin = "BTC" | "ETH" | "XMR"; + +type PendingDeposit = { + id: string; + coin: Coin; + txid: string; + ts: number; + status: "pending" | "credited"; + ref?: string; +}; + +const PENDING_KEY = "cyberlux-pending-deposits-v1"; + +function loadPending(): PendingDeposit[] { + if (typeof window === "undefined") return []; + try { return JSON.parse(localStorage.getItem(PENDING_KEY) ?? "[]") as PendingDeposit[]; } + catch { return []; } +} + +function savePending(d: PendingDeposit[]) { + if (typeof window !== "undefined") localStorage.setItem(PENDING_KEY, JSON.stringify(d)); +} + +function shortId() { + return Math.random().toString(36).slice(2, 10).toUpperCase(); +} + +const COIN_COLORS: Record = { + BTC: "text-amber-400", + ETH: "text-blue-400", + XMR: "text-orange-400", +}; + +const COIN_BORDER: Record = { + BTC: "border-amber-500/30", + ETH: "border-blue-500/30", + XMR: "border-orange-500/30", +}; + +const COIN_GLOW: Record = { + BTC: "bg-amber-500/10", + ETH: "bg-blue-500/10", + XMR: "bg-orange-500/10", +}; + export default function AddFundsPage() { const router = useRouter(); const { user, hydrated } = useAccount(); - const { usdStoreCredit, verifyBtcDeposit } = useWallet(); + const { usdStoreCredit, luxCredits, verifyBtcDeposit } = useWallet(); + + const [activeCoin, setActiveCoin] = useState("BTC"); const [txid, setTxid] = useState(""); const [busy, setBusy] = useState(false); - const [msg, setMsg] = useState<{ kind: "ok" | "err"; text: string } | null>(null); + const [msg, setMsg] = useState<{ kind: "ok" | "err" | "pending"; text: string; ref?: string } | null>(null); + const [pendingDeposits, setPendingDeposits] = useState([]); + const [copied, setCopied] = useState(null); + const processorUrl = (process.env.NEXT_PUBLIC_BITCOIN_CHECKOUT_URL || "").trim(); + const btcAddr = getMerchantBtcAddress(); + const btcReady = isMerchantBtcConfigured(); + const ethAddr = (process.env.NEXT_PUBLIC_ETH_ADDRESS || "").trim(); + const xmrAddr = (process.env.NEXT_PUBLIC_XMR_ADDRESS || "").trim(); + + const addresses: Record = { + BTC: btcAddr, + ETH: ethAddr, + XMR: xmrAddr, + }; + + const addrReady: Record = { + BTC: btcReady, + ETH: Boolean(ethAddr), + XMR: Boolean(xmrAddr), + }; + useEffect(() => { if (hydrated && !user) router.replace("/sign-in?next=/account/add-funds"); }, [hydrated, user, router]); - if (!hydrated || !user) { - return ( -
- Loading… -
- ); - } + useEffect(() => { + setPendingDeposits(loadPending()); + }, []); - const merchant = getMerchantBtcAddress(); - const ready = isMerchantBtcConfigured(); + const copyAddr = async (coin: Coin) => { + const addr = addresses[coin]; + if (!addr) return; + try { + await navigator.clipboard.writeText(addr); + setCopied(coin); + setTimeout(() => setCopied(null), 1800); + } catch { /* ignore */ } + }; - const onVerify = async (e: FormEvent) => { + const onVerifyBtc = async (e: FormEvent) => { e.preventDefault(); + if (!txid.trim()) return; setMsg(null); setBusy(true); try { @@ -41,118 +112,365 @@ export default function AddFundsPage() { if (res.ok) { setMsg({ kind: "ok", - text: `Credited $${(res.creditedUsd ?? 0).toFixed(2)} USD to @${user.username}.`, + text: `✓ $${(res.creditedUsd ?? 0).toFixed(2)} USD credited to @${user!.username}. Balance updated.`, }); setTxid(""); } else { - setMsg({ kind: "err", text: res.error || "Verification failed" }); + setMsg({ kind: "err", text: res.error || "Verification failed. Check the txid and try again." }); } } finally { setBusy(false); } }; + const onSubmitManual = async (e: FormEvent) => { + e.preventDefault(); + if (!txid.trim() || !user) return; + setMsg(null); + setBusy(true); + try { + const res = await fetch("/api/pending-deposit", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ coin: activeCoin, txid: txid.trim(), handle: user.username }), + }); + const json = (await res.json()) as { ok: boolean; message?: string; ref?: string; error?: string }; + if (json.ok) { + const entry: PendingDeposit = { + id: shortId(), + coin: activeCoin, + txid: txid.trim(), + ts: Date.now(), + status: "pending", + ref: json.ref, + }; + const updated = [entry, ...pendingDeposits]; + setPendingDeposits(updated); + savePending(updated); + setMsg({ kind: "pending", text: json.message ?? "Deposit request submitted for manual review.", ref: json.ref }); + setTxid(""); + } else { + setMsg({ kind: "err", text: json.error ?? "Submission failed." }); + } + } catch { + setMsg({ kind: "err", text: "Network error. Try again." }); + } finally { + setBusy(false); + } + }; + + if (!hydrated || !user) { + return ( +
+ Loading… +
+ ); + } + return (
-
-

account

-

Add funds

-

- One CyberLux handle — same USD balance on hub, market, forum, exchange, and checkout. Send Bitcoin to - your configured receiving address, wait for at least one confirmation, then paste the transaction id - here. We verify on-chain pays to{" "} - MERCHANT_BTC_ADDRESS and credit USD at spot (see{" "} - /api/btc/verify). -

+
-
-
-
-

Current balance

-

${usdStoreCredit.toFixed(2)} USD

+ {/* Header */} +
+

+ @{user.username} · deposit desk +

+

Fund Your Account

+

+ Send crypto to the address below. We verify the transaction on-chain, convert to USD at current + market rate, and credit your account instantly. You no longer + hold the crypto — we do. When you make a purchase we pay the vendor with it on your behalf. +

+
+ + {/* How it works */} +
+ {[ + { step: "1", icon: "₿", title: "You send crypto", body: "Send BTC, ETH, or XMR to our receiving address." }, + { step: "2", icon: "📊", title: "We verify & convert", body: "On-chain confirmation. Credited at live market USD rate." }, + { step: "3", icon: "🛒", title: "You shop", body: "Spend USD credits across every CyberLux surface. We pay vendors in crypto." }, + ].map((s) => ( +
+
+
+ {s.step} +
+ {s.icon} +
+
{s.title}
+

{s.body}

+
+ ))} +
+ + {/* Balance card */} +
+
+
+
Current balance — @{user.username}
+
${usdStoreCredit.toFixed(2)} USD
+
{luxCredits.toLocaleString()} LUX
+
+
+ + Spend → + + + Passes +
- - Spend at checkout → -
- {processorUrl ? ( - - ) : null} + {coin} + + ))} +
-
-

On-chain deposit

- {!ready ? ( -

- Set NEXT_PUBLIC_MERCHANT_BTC_ADDRESS and{" "} - MERCHANT_BTC_ADDRESS in{" "} - .env.local, restart Next, then reload this page. -

+ {/* Deposit address */} +
+
+

+ {activeCoin} Deposit Address +

+ {activeCoin === "BTC" && ( + + Auto-verify + + )} + {(activeCoin === "ETH" || activeCoin === "XMR") && ( + + Manual review + + )} +
+ + {!addrReady[activeCoin] ? ( +
+

Address not configured

+

+ Set{" "} + + {activeCoin === "BTC" + ? "NEXT_PUBLIC_MERCHANT_BTC_ADDRESS" + : activeCoin === "ETH" + ? "NEXT_PUBLIC_ETH_ADDRESS" + : "NEXT_PUBLIC_XMR_ADDRESS"} + {" "} + in .env.local and restart Next. +

+
) : ( <> -

- Send from any wallet. After one confirmation, paste the 64-character txid. Credit is computed from - outputs paying this address only. -

-
- {merchant} +
+ {addresses[activeCoin]}
-
void onVerify(e)} className="mt-6 space-y-3"> - - setTxid(e.target.value.replace(/\s+/g, ""))} - className="w-full rounded-xl border border-white/10 bg-transparent px-4 py-3 font-mono text-sm" - placeholder="64 hex characters" - autoComplete="off" - /> - - {msg ? ( -

{msg.text}

- ) : null} -
+ + + {activeCoin === "BTC" && ( +
+ Send any amount · 1 confirmation required · credited at CoinGecko spot rate +
+ )} + {activeCoin === "ETH" && ( +
+ Send ERC-20 or native ETH · submit txhash below for manual review · credited within 2–24h +
+ )} + {activeCoin === "XMR" && ( +
+ Monero transactions take ~20 min · submit txid below for manual review · credited within 2–24h +
+ )} )}
-

- - Dashboard - - {" · "} - - Cross-onion identity - - {" · "} - - Hub - -

+ {/* Optional hosted processor (BTCPay etc) */} + {activeCoin === "BTC" && processorUrl && ( +
+
+
+
Payment Processor
+

+ Pay through the hosted checkout page (BTCPay or custom processor). +

+
+ + Open → + +
+
+ )} + + {/* Verify / submit form */} + {addrReady[activeCoin] && ( +
+

+ {activeCoin === "BTC" ? "Verify BTC Transaction" : `Submit ${activeCoin} Deposit for Review`} +

+

+ {activeCoin === "BTC" + ? "After at least 1 confirmation, paste the 64-character transaction ID. We check mempool.space and credit immediately." + : `After sending, paste the transaction hash. Our team verifies on-chain and credits your @${user.username} account. You'll see it in your pending history below.`} +

+
activeCoin === "BTC" ? void onVerifyBtc(e) : void onSubmitManual(e)} + className="space-y-4" + > + setTxid(e.target.value.trim())} + placeholder={ + activeCoin === "BTC" + ? "Bitcoin txid — 64 hex characters" + : activeCoin === "ETH" + ? "Ethereum txhash — 0x…" + : "Monero txid" + } + className="w-full rounded-xl border border-white/10 bg-transparent px-4 py-3 font-mono text-sm placeholder-zinc-600 focus:border-neon-cyan/40 focus:outline-none" + autoComplete="off" + maxLength={100} + /> + +
+ + {msg && ( +
+

{msg.text}

+ {msg.ref && ( +

Reference: {msg.ref}

+ )} +
+ )} +
+ )} + + {/* Pending history */} + {pendingDeposits.length > 0 && ( +
+

Pending Manual Reviews

+
+ {pendingDeposits.map((d) => ( +
+
+ {d.coin} + + {d.txid.slice(0, 12)}…{d.txid.slice(-8)} + + {d.ref && ( + ref: {d.ref} + )} +
+
+ + {d.status} + + {new Date(d.ts).toLocaleDateString()} +
+
+ ))} +
+
+ )} + + {/* Important notes */} +
+
Important — Read Before Depositing
+
    +
  • + + Always double-check the address against the one shown here before sending. Verify on each new session. +
  • +
  • + + BTC deposits are credited automatically after 1 confirmation (~10 min). ETH and XMR require manual review (2–24h). +
  • +
  • + + Your USD credit balance is stored per-handle in your browser. Export your account bundle from + {" "}/dashboard{" "} + before clearing site data. +
  • +
  • + + Once credited, funds cannot be withdrawn — they are USD store credit for purchases only. +
  • +
  • + + Minimum deposit: no minimum enforced, but small amounts may be consumed by exchange rate rounding. +
  • +
+
+ +
+ Dashboard + Checkout + Digital Passes + Cross-onion identity + Hub +
); diff --git a/app/api/pending-deposit/route.ts b/app/api/pending-deposit/route.ts new file mode 100644 index 0000000..84f5a42 --- /dev/null +++ b/app/api/pending-deposit/route.ts @@ -0,0 +1,44 @@ +/** + * /api/pending-deposit + * + * Stores a manual-review deposit request (ETH or XMR) server-side in a + * simple append-only JSON file. In production you'd swap this for a DB + * write. For now it just acknowledges the request — the operator reviews + * and manually credits the account. + */ +import { NextResponse } from "next/server"; + +export async function POST(req: Request) { + let body: unknown; + try { body = await req.json(); } catch { + return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 }); + } + + const { coin, txid, handle } = (body as Record) ?? {}; + + if (!coin || !txid || !handle) { + return NextResponse.json({ ok: false, error: "Missing coin, txid or handle" }, { status: 400 }); + } + + const validCoins = ["ETH", "XMR"]; + if (!validCoins.includes(coin.toUpperCase())) { + return NextResponse.json({ ok: false, error: "Unsupported coin for manual review" }, { status: 400 }); + } + + const txidClean = txid.trim(); + if (txidClean.length < 20) { + return NextResponse.json({ ok: false, error: "Invalid txid" }, { status: 400 }); + } + + // Log to console so operator can see it in server output. + // In production: write to DB or send webhook. + console.log( + `[PENDING DEPOSIT] coin=${coin.toUpperCase()} handle=@${handle} txid=${txidClean} ts=${new Date().toISOString()}` + ); + + return NextResponse.json({ + ok: true, + message: `Manual review request logged for @${handle}. Operator will verify and credit your account.`, + ref: `${coin.toUpperCase()}-${txidClean.slice(0, 8).toUpperCase()}`, + }); +} diff --git a/app/checkout/page.tsx b/app/checkout/page.tsx index f6fde79..d9bacc3 100644 --- a/app/checkout/page.tsx +++ b/app/checkout/page.tsx @@ -1,6 +1,7 @@ import Navbar from "@/components/Navbar"; import ParticleBackground from "@/components/ParticleBackground"; import CheckoutFlow from "@/components/CheckoutFlow"; +import DepositWidget from "@/components/DepositWidget"; import Link from "next/link"; export default function CheckoutPage() { @@ -9,52 +10,68 @@ export default function CheckoutPage() {
-
+
- ← Back to Sanctuary + ← Back to Market

CHECKOUT RITUAL

- Each step is designed to feel like a ceremonial unlocking. Your data is encrypted in real‑time, and your payment is transformed into a visual experience. + Pay with your USD balance — funded by Bitcoin deposit. Balance is credited after on-chain + verification. We hold your crypto and pay vendors on your behalf.

+ + {/* Deposit reminder before CheckoutFlow */} +
+ +
+ +
+
+
+

Bitcoin-funded USD

+

+ Send BTC to your deposit address at Add Funds. + After 1 confirmation we credit USD at live spot rate. +

+
🔐
-

End‑to‑End Encryption

+

Per-handle balance

- Your payment details never touch our servers. They are encrypted client‑side before transmission. + USD credit is tied to your CyberLux handle — same balance across hub, market, forum, and exchange. + Export your account from /dashboard to back it up.

-
-

Instant Crypto Conversion

+
🧾
+

Vault receipt

- Real‑time exchange rates ensure you pay the exact amount, with no hidden fees. -

-
-
-
🎨
-

Digital Art Receipt

-

- Each completed order generates a unique piece of generative art stored in your vault. + Every completed order writes a signed receipt to your Vault. + LUX credits earned on each purchase.

- Crypto checkout uses your USD balance after a Bitcoin payment is{" "} - verified on-chain. Configure your receiving address in{" "} - .env.local. Guest/card paths are demo-only. + Payments use USD balance funded by verified on-chain Bitcoin deposits. Configure merchant + address in .env.local. + ETH and XMR deposits go through manual review.

+
+ Add Funds + Dashboard + Market +
); -} \ No newline at end of file +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index e09c5f1..c6cb50a 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -4,6 +4,7 @@ import { FormEvent, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import Navbar from "@/components/Navbar"; +import DepositWidget from "@/components/DepositWidget"; import { useAccount } from "@/contexts/AccountContext"; import { useWallet } from "@/contexts/WalletContext"; import { computeUserActivityStats } from "@/lib/userActivityStats"; @@ -100,33 +101,21 @@ export default function DashboardPage() { -
-

Balance

-

- USD is credited after verified Bitcoin deposits to your merchant address. LUX is loyalty currency (e.g. - completed checkouts). -

-
    -
  • - LUX +
    + +
    +
    + Vault keys + {vault.keys.length} +
    +
    + LUX credits {luxCredits.toLocaleString()} -
  • -
  • - USD (spendable) +
+
+ USD balance ${usdStoreCredit.toFixed(2)} - -
  • - Vault keys - {vault.keys.length} -
  • - -
    - - Add funds (BTC) → - - - Checkout → - +
    diff --git a/components/DepositWidget.tsx b/components/DepositWidget.tsx new file mode 100644 index 0000000..91b9107 --- /dev/null +++ b/components/DepositWidget.tsx @@ -0,0 +1,85 @@ +"use client"; + +import Link from "next/link"; +import { useWallet } from "@/contexts/WalletContext"; +import { useAccount } from "@/contexts/AccountContext"; +import { isMerchantBtcConfigured } from "@/lib/merchantBtc"; + +type DepositWidgetProps = { + compact?: boolean; +}; + +export default function DepositWidget({ compact = false }: DepositWidgetProps) { + const { usdStoreCredit, luxCredits } = useWallet(); + const { user } = useAccount(); + const btcReady = isMerchantBtcConfigured(); + + if (compact) { + return ( +
    +
    +
    +
    Your balance
    +
    + ${usdStoreCredit.toFixed(2)} + USD +
    +
    {luxCredits.toLocaleString()} LUX
    +
    + + Deposit crypto → + +
    + {!user && ( +

    + Sign in to link deposits to your handle. +

    + )} + {!btcReady && ( +

    + Operator: set NEXT_PUBLIC_MERCHANT_BTC_ADDRESS in .env.local to enable deposits. +

    + )} +
    + ); + } + + return ( +
    +
    +
    +
    Account balance
    +
    ${usdStoreCredit.toFixed(2)}
    +
    {luxCredits.toLocaleString()} LUX
    +
    + + Add crypto → + +
    + +
    +
    How it works
    +
    +
    + 1. + You send crypto to our receiving address. +
    +
    + 2. + We verify on-chain and credit your account at market USD rate. +
    +
    + 3. + You shop with USD credits. We hold the crypto and pay vendors on your behalf. +
    +
    +
    +
    + ); +}