"use client"; import { createContext, useCallback, useContext, useEffect, useMemo, useState, ReactNode } from "react"; import type { VaultReceipt, VaultState } from "@/lib/metaGame"; import { addKey, addReceipt, defaultVaultState, loadVaultState, saveVaultState, setFlag } from "@/lib/metaGame"; import { getLedgerRow, migrateLegacyDeviceLedgerIfNeeded, setLedgerRow } from "@/lib/storeCreditStorage"; import { useAccount } from "@/contexts/AccountContext"; interface WalletContextType { usdStoreCredit: number; luxCredits: number; spendLuxCredits: (amount: number) => boolean; earnLuxCredits: (amount: number) => void; spendUsdStoreCredit: (amount: number) => boolean; verifyBtcDeposit: (txid: string) => Promise<{ ok: boolean; error?: string; creditedUsd?: number }>; creditBtcPayInvoice: (invoiceId: string, usdAmount: number) => { ok: boolean; error?: string }; vault: VaultState; collectKey: (key: string) => boolean; addVaultReceipt: (receipt: VaultReceipt) => void; setVaultFlag: (flag: string, value: boolean) => void; } const WalletContext = createContext(undefined); export const useWallet = () => { const context = useContext(WalletContext); if (!context) { throw new Error("useWallet must be used within WalletProvider"); } return context; }; interface WalletProviderProps { children: ReactNode; } export const WalletProvider = ({ children }: WalletProviderProps) => { const { user } = useAccount(); const handle = user?.username ?? null; const [usdStoreCredit, setUsdStoreCredit] = useState(0); const [luxCredits, setLuxCredits] = useState(0); const [vault, setVault] = useState(() => defaultVaultState()); useEffect(() => { if (!handle) { setUsdStoreCredit(0); setLuxCredits(0); return; } migrateLegacyDeviceLedgerIfNeeded(handle); const row = getLedgerRow(handle); setUsdStoreCredit(row.usd); setLuxCredits(row.lux); }, [handle]); useEffect(() => { setVault(loadVaultState(handle)); }, [handle]); useEffect(() => { saveVaultState(handle, vault); }, [vault, handle]); const spendLuxCredits = useCallback((amount: number) => { if (!handle) return false; const safeAmount = Number.isFinite(amount) ? Math.max(0, Math.floor(amount)) : 0; if (safeAmount <= 0) return true; const row = getLedgerRow(handle); if (row.lux < safeAmount) return false; const nextLux = row.lux - safeAmount; setLedgerRow(handle, { usd: row.usd, lux: nextLux, claimedTxids: row.claimedTxids }); setLuxCredits(nextLux); return true; }, [handle]); const earnLuxCredits = useCallback( (amount: number) => { if (!handle) return; const safeAmount = Number.isFinite(amount) ? Math.max(0, Math.floor(amount)) : 0; if (safeAmount <= 0) return; const row = getLedgerRow(handle); const nextLux = row.lux + safeAmount; setLedgerRow(handle, { usd: row.usd, lux: nextLux, claimedTxids: row.claimedTxids }); setLuxCredits(nextLux); }, [handle], ); const spendUsdStoreCredit = useCallback( (amount: number) => { if (!handle) return false; const safe = Number.isFinite(amount) ? Math.max(0, Math.round(amount * 100) / 100) : 0; if (safe <= 0) return true; const row = getLedgerRow(handle); if (row.usd + 1e-9 < safe) return false; const nextUsd = Math.round((row.usd - safe) * 100) / 100; setLedgerRow(handle, { usd: nextUsd, lux: row.lux, claimedTxids: row.claimedTxids }); setUsdStoreCredit(nextUsd); return true; }, [handle], ); const verifyBtcDeposit = useCallback( async (txid: string) => { if (!handle) { return { ok: false, error: "Sign in so deposits credit your handle’s balance." }; } const normalized = String(txid || "").trim(); if (!/^[a-fA-F0-9]{64}$/.test(normalized)) { return { ok: false, error: "Enter a valid 64-character transaction id" }; } const row = getLedgerRow(handle); if (row.claimedTxids.includes(normalized)) { return { ok: false, error: "This transaction was already used to add credit" }; } try { const res = await fetch("/api/btc/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ txid: normalized }), }); const data = (await res.json()) as { ok?: boolean; error?: string; creditedUsd?: number; }; if (!data.ok || typeof data.creditedUsd !== "number") { return { ok: false, error: data.error || "Verification failed" }; } const add = Math.round(data.creditedUsd * 100) / 100; const nextUsd = Math.round((row.usd + add) * 100) / 100; const claimed = [...row.claimedTxids, normalized]; setLedgerRow(handle, { usd: nextUsd, lux: row.lux, claimedTxids: claimed }); setUsdStoreCredit(nextUsd); return { ok: true, creditedUsd: add }; } catch { return { ok: false, error: "Network error while verifying" }; } }, [handle], ); const creditBtcPayInvoice = useCallback( (invoiceId: string, usdAmount: number) => { if (!handle) return { ok: false, error: "Sign in first" }; const id = String(invoiceId || "").trim(); if (!id) return { ok: false, error: "Invalid invoice ID" }; const safe = Number.isFinite(usdAmount) ? Math.max(0, Math.round(usdAmount * 100) / 100) : 0; if (safe <= 0) return { ok: false, error: "Invalid amount" }; const row = getLedgerRow(handle); if (row.claimedTxids.includes(id)) return { ok: false, error: "Invoice already credited" }; const nextUsd = Math.round((row.usd + safe) * 100) / 100; setLedgerRow(handle, { usd: nextUsd, lux: row.lux, claimedTxids: [...row.claimedTxids, id] }); setUsdStoreCredit(nextUsd); return { ok: true }; }, [handle], ); const collectKey = (key: string) => { const trimmed = String(key || "").trim(); if (!trimmed) return false; if (vault.keys.includes(trimmed)) return false; setVault((v) => addKey(v, trimmed)); return true; }; const addVaultReceipt = (receipt: VaultReceipt) => { setVault((v) => addReceipt(v, receipt)); }; const setVaultFlag = (flag: string, value: boolean) => { const f = String(flag || "").trim(); if (!f) return; setVault((v) => setFlag(v, f, Boolean(value))); }; const value = useMemo( () => ({ usdStoreCredit, luxCredits, spendLuxCredits, earnLuxCredits, spendUsdStoreCredit, verifyBtcDeposit, creditBtcPayInvoice, vault, collectKey, addVaultReceipt, setVaultFlag, }), [ usdStoreCredit, luxCredits, spendLuxCredits, earnLuxCredits, spendUsdStoreCredit, verifyBtcDeposit, creditBtcPayInvoice, vault, ], ); return {children}; };