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,
  };
}