Harden onion boot flow and deepen site surfaces
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
This commit is contained in:
196
contexts/WalletContext.tsx
Normal file
196
contexts/WalletContext.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
"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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user