Add persistent onion key backup and restore, improve startup resilience, and flesh out the major site verticals with richer navigation, search coverage, and operator documentation. Made-with: Cursor
196 lines
5.8 KiB
TypeScript
196 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
import { createContext, 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 {
|
|
loadClaimedTxids,
|
|
loadUsdStoreCredit,
|
|
saveClaimedTxids,
|
|
saveUsdStoreCredit,
|
|
} from "@/lib/storeCreditStorage";
|
|
|
|
interface WalletContextType {
|
|
isConnected: boolean;
|
|
address: string | null;
|
|
chainId: number | null;
|
|
connect: () => Promise<void>;
|
|
disconnect: () => void;
|
|
balance: string;
|
|
luxCredits: number;
|
|
usdStoreCredit: number;
|
|
spendLuxCredits: (amount: number) => boolean;
|
|
earnLuxCredits: (amount: number) => void;
|
|
spendUsdStoreCredit: (amount: number) => boolean;
|
|
verifyBtcDeposit: (txid: string) => Promise<{ ok: boolean; error?: string; creditedUsd?: number }>;
|
|
vault: VaultState;
|
|
collectKey: (key: string) => boolean;
|
|
addVaultReceipt: (receipt: VaultReceipt) => void;
|
|
setVaultFlag: (flag: string, value: boolean) => void;
|
|
}
|
|
|
|
const WalletContext = createContext<WalletContextType | undefined>(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 [isConnected, setIsConnected] = useState(false);
|
|
const [address, setAddress] = useState<string | null>(null);
|
|
const [chainId, setChainId] = useState<number | null>(null);
|
|
const [balance, setBalance] = useState("0.0");
|
|
const [luxCredits, setLuxCredits] = useState(0);
|
|
const [usdStoreCredit, setUsdStoreCredit] = useState(0);
|
|
const [usdHydrated, setUsdHydrated] = useState(false);
|
|
const [vault, setVault] = useState<VaultState>(() => defaultVaultState());
|
|
|
|
const mockAddress = "0x71C7...e4a2";
|
|
const mockChainId = 1;
|
|
const mockBalance = "2.45";
|
|
const startingLuxCredits = 5000;
|
|
|
|
const connect = async () => {
|
|
// Simulate wallet connection delay
|
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
setIsConnected(true);
|
|
setAddress(mockAddress);
|
|
setChainId(mockChainId);
|
|
setBalance(mockBalance);
|
|
setLuxCredits(startingLuxCredits);
|
|
};
|
|
|
|
const disconnect = () => {
|
|
setIsConnected(false);
|
|
setAddress(null);
|
|
setChainId(null);
|
|
setBalance("0.0");
|
|
setLuxCredits(0);
|
|
};
|
|
|
|
useEffect(() => {
|
|
setVault(loadVaultState());
|
|
setUsdStoreCredit(loadUsdStoreCredit());
|
|
setUsdHydrated(true);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
saveVaultState(vault);
|
|
}, [vault]);
|
|
|
|
useEffect(() => {
|
|
if (!usdHydrated) return;
|
|
saveUsdStoreCredit(usdStoreCredit);
|
|
}, [usdStoreCredit, usdHydrated]);
|
|
|
|
const spendLuxCredits = (amount: number) => {
|
|
const safeAmount = Number.isFinite(amount) ? Math.max(0, Math.floor(amount)) : 0;
|
|
if (safeAmount <= 0) return true;
|
|
if (luxCredits < safeAmount) return false;
|
|
setLuxCredits((c) => c - safeAmount);
|
|
return true;
|
|
};
|
|
|
|
const earnLuxCredits = (amount: number) => {
|
|
const safeAmount = Number.isFinite(amount) ? Math.max(0, Math.floor(amount)) : 0;
|
|
if (safeAmount <= 0) return;
|
|
setLuxCredits((c) => c + safeAmount);
|
|
};
|
|
|
|
const spendUsdStoreCredit = (amount: number) => {
|
|
const safe = Number.isFinite(amount) ? Math.max(0, Math.round(amount * 100) / 100) : 0;
|
|
if (safe <= 0) return true;
|
|
if (usdStoreCredit + 1e-9 < safe) return false;
|
|
setUsdStoreCredit((u) => Math.round((u - safe) * 100) / 100);
|
|
return true;
|
|
};
|
|
|
|
const verifyBtcDeposit = async (txid: string) => {
|
|
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 claimed = loadClaimedTxids();
|
|
if (claimed.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" };
|
|
}
|
|
claimed.push(normalized);
|
|
saveClaimedTxids(claimed);
|
|
const add = Math.round(data.creditedUsd * 100) / 100;
|
|
setUsdStoreCredit((u) => Math.round((u + add) * 100) / 100);
|
|
return { ok: true, creditedUsd: add };
|
|
} catch {
|
|
return { ok: false, error: "Network error while verifying" };
|
|
}
|
|
};
|
|
|
|
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(
|
|
() => ({
|
|
isConnected,
|
|
address,
|
|
chainId,
|
|
connect,
|
|
disconnect,
|
|
balance,
|
|
luxCredits,
|
|
usdStoreCredit,
|
|
spendLuxCredits,
|
|
earnLuxCredits,
|
|
spendUsdStoreCredit,
|
|
verifyBtcDeposit,
|
|
vault,
|
|
collectKey,
|
|
addVaultReceipt,
|
|
setVaultFlag,
|
|
}),
|
|
[isConnected, address, chainId, balance, luxCredits, usdStoreCredit, vault],
|
|
);
|
|
|
|
return (
|
|
<WalletContext.Provider
|
|
value={value}
|
|
>
|
|
{children}
|
|
</WalletContext.Provider>
|
|
);
|
|
}; |