Spaces:
Build error
Build error
File size: 6,957 Bytes
1295969 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | import { useState, useCallback } from "react";
import type { WalletState, ChainId } from "@/types/wallet";
const INITIAL_STATE: WalletState = {
connected: false,
address: "",
chain: null,
walletType: null,
};
const AUTH_TOKEN_KEY = "retrosync_auth_token";
const AUTH_ADDRESS_KEY = "retrosync_auth_address";
// ββ Auth token helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function getAuthToken(): string | null {
return sessionStorage.getItem(AUTH_TOKEN_KEY);
}
export function clearAuthToken(): void {
sessionStorage.removeItem(AUTH_TOKEN_KEY);
sessionStorage.removeItem(AUTH_ADDRESS_KEY);
}
function storeAuthToken(token: string, address: string): void {
sessionStorage.setItem(AUTH_TOKEN_KEY, token);
sessionStorage.setItem(AUTH_ADDRESS_KEY, address);
}
/** Return headers for authenticated API calls */
export function authHeaders(): Record<string, string> {
const token = getAuthToken();
return token ? { Authorization: `Bearer ${token}` } : {};
}
// ββ Challenge-response authentication ββββββββββββββββββββββββββββββββββββββββ
/**
* Authenticate with the backend using a wallet signature challenge.
*
* Flow:
* 1. Fetch a random nonce from GET /api/auth/challenge/{address}
* 2. Sign the nonce with the connected wallet
* 3. POST the signature to /api/auth/verify β receive JWT
* 4. Store the JWT in sessionStorage for subsequent API calls
*
* Supports:
* - TronLink on BTTC (EVM): uses window.tronWeb.eth.personal.sign
* - TronLink on Tron mainnet: uses window.tronWeb.trx.signMessageV2
* - Any window.ethereum wallet (MetaMask, Coinbase): uses personal_sign
*/
async function authenticateWithServer(
address: string,
walletType: "tronlink" | "evm"
): Promise<string> {
// Step 1: Get challenge nonce
const challengeRes = await fetch(`/api/auth/challenge/${address.toLowerCase()}`);
if (!challengeRes.ok) {
throw new Error(`Challenge request failed: ${challengeRes.status}`);
}
const { challenge_id, nonce } = await challengeRes.json();
// Step 2: Sign nonce with wallet
let signature: string;
if (walletType === "evm" && window.ethereum) {
// EVM personal_sign (EIP-191): MetaMask, Coinbase, TronLink on BTTC
signature = (await window.ethereum.request({
method: "personal_sign",
params: [nonce, address],
})) as string;
} else if (window.tronWeb?.trx?.signMessageV2) {
// TronLink on Tron mainnet: signMessageV2
signature = await window.tronWeb.trx.signMessageV2(nonce);
} else if (window.tronWeb?.trx?.sign) {
// Fallback: older TronLink sign API (browser-compatible hex encoding)
const enc = new TextEncoder();
const bytes = enc.encode(nonce);
const hexMsg = "0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
signature = await window.tronWeb.trx.sign(hexMsg);
} else {
throw new Error("No supported wallet signing method found.");
}
if (!signature) {
throw new Error("Signing was cancelled or failed.");
}
// Step 3: Verify with backend
const verifyRes = await fetch("/api/auth/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ challenge_id, address: address.toLowerCase(), signature }),
});
if (!verifyRes.ok) {
const text = await verifyRes.text().catch(() => "");
throw new Error(`Signature verification failed (${verifyRes.status}): ${text}`);
}
const { token } = await verifyRes.json();
if (!token) {
throw new Error("Backend did not return an auth token.");
}
storeAuthToken(token, address);
return token;
}
// ββ Hook ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function useWallet() {
const [wallet, setWallet] = useState<WalletState>(INITIAL_STATE);
const [isConnecting, setIsConnecting] = useState(false);
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [error, setError] = useState("");
const connectTronLink = useCallback(async (chain: ChainId) => {
setIsConnecting(true);
setError("");
try {
if (!window.tronLink && !window.tronWeb) {
throw new Error(
"TronLink is not installed. Please install the TronLink extension from tronlink.org"
);
}
if (window.tronLink) {
await window.tronLink.request({ method: "tron_requestAccounts" });
}
// Wait briefly for tronWeb to initialise
await new Promise((r) => setTimeout(r, 500));
if (!window.tronWeb?.ready) {
throw new Error(
"TronLink is locked. Please unlock your wallet and try again."
);
}
const address = window.tronWeb.defaultAddress.base58;
if (!address) {
throw new Error(
"No account found. Please create an account in TronLink first."
);
}
setWallet({ connected: true, address, chain, walletType: "tronlink" });
// Authenticate with the backend (non-blocking β failures are non-fatal)
setIsAuthenticating(true);
try {
const isEvm = chain === "bttc";
await authenticateWithServer(address, isEvm ? "evm" : "tronlink");
} catch (authErr) {
console.warn("Backend auth failed (API calls may be limited):", authErr);
} finally {
setIsAuthenticating(false);
}
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : "Failed to connect wallet.";
setError(message);
} finally {
setIsConnecting(false);
}
}, []);
const connectWalletConnect = useCallback(async (_chain: ChainId) => {
setError("WalletConnect support is coming soon. Please use TronLink for now.");
}, []);
const disconnect = useCallback(() => {
setWallet(INITIAL_STATE);
setError("");
clearAuthToken();
}, []);
const shortenAddress = (addr: string) =>
addr ? `${addr.slice(0, 6)}\u2026${addr.slice(-4)}` : "";
const connectCoinbase = useCallback(async (_chain: ChainId) => {
// SECURITY FIX: Removed hardcoded stub address "0xCB0000...0001" that was
// shared by ALL users, causing identity confusion and financial fraud.
// Coinbase Wallet SDK integration is required before enabling this flow.
setError(
"Coinbase Wallet integration is being configured. Please use TronLink for now."
);
}, []);
return {
wallet,
isConnecting,
isAuthenticating,
error,
connectTronLink,
connectWalletConnect,
connectCoinbase,
disconnect,
shortenAddress,
setError,
getAuthToken,
authHeaders,
};
}
|