create-sbc-app 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1701 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { TurnkeyProvider, useTurnkey } from '@turnkey/sdk-react';
3
+ import { createPublicClient, http, getAddress, parseSignature, WalletClient, PublicClient, Chain } from 'viem';
4
+ import { baseSepolia, base } from 'viem/chains';
5
+ import { useSbcTurnkey } from '@stablecoin.xyz/react';
6
+ import { parseUnits, encodeFunctionData, erc20Abi } from 'viem';
7
+ import './index.css';
8
+
9
+ const chain = (import.meta.env.VITE_CHAIN === 'base') ? base : baseSepolia;
10
+ const rpcUrl = import.meta.env.VITE_RPC_URL;
11
+
12
+ const SBC_TOKEN_ADDRESS = (chain: Chain) => {
13
+ if (chain.id === baseSepolia.id) return '0xf9FB20B8E097904f0aB7d12e9DbeE88f2dcd0F16';
14
+ if (chain.id === base.id) return '0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798';
15
+ throw new Error('Unsupported chain');
16
+ };
17
+
18
+ const SBC_DECIMALS = (chain: Chain) => {
19
+ if (chain.id === baseSepolia.id) return 6;
20
+ if (chain.id === base.id) return 18;
21
+ throw new Error('Unsupported chain');
22
+ };
23
+
24
+ const chainExplorer = (chain: Chain) => {
25
+ if (chain.id === baseSepolia.id) return 'https://sepolia.basescan.org';
26
+ if (chain.id === base.id) return 'https://basescan.org';
27
+ throw new Error('Unsupported chain');
28
+ };
29
+
30
+ const publicClient = createPublicClient({ chain, transport: http() });
31
+
32
+ const erc20PermitAbi = [
33
+ ...erc20Abi,
34
+ {
35
+ "inputs": [{ "internalType": "address", "name": "owner", "type": "address" }],
36
+ "name": "nonces",
37
+ "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
38
+ "stateMutability": "view",
39
+ "type": "function"
40
+ }
41
+ ];
42
+
43
+ const permitAbi = [{
44
+ "inputs": [
45
+ { "internalType": "address", "name": "owner", "type": "address" },
46
+ { "internalType": "address", "name": "spender", "type": "address" },
47
+ { "internalType": "uint256", "name": "value", "type": "uint256" },
48
+ { "internalType": "uint256", "name": "deadline", "type": "uint256" },
49
+ { "internalType": "uint8", "name": "v", "type": "uint8" },
50
+ { "internalType": "bytes32", "name": "r", "type": "bytes32" },
51
+ { "internalType": "bytes32", "name": "s", "type": "bytes32" }
52
+ ],
53
+ "name": "permit",
54
+ "outputs": [],
55
+ "stateMutability": "nonpayable",
56
+ "type": "function"
57
+ }];
58
+
59
+ // Turnkey SDK configuration - passkey authentication only
60
+ const turnkeyConfig = {
61
+ apiBaseUrl: import.meta.env.VITE_TURNKEY_API_BASE_URL || 'https://api.turnkey.com',
62
+ defaultOrganizationId: '', // Not used for sub-org pattern
63
+ rpId: import.meta.env.VITE_TURNKEY_RPID || window.location.hostname,
64
+ // Don't set iframeUrl - loads on demand when passkey auth is actually used
65
+ // Don't set ethereumWalletInterface - we handle MetaMask directly in wallet flow
66
+ };
67
+
68
+ interface AccountHistoryItem {
69
+ subOrgId: string;
70
+ email?: string;
71
+ walletAddress?: string;
72
+ authType: 'passkey' | 'wallet';
73
+ lastUsed: number; // timestamp
74
+ }
75
+
76
+ function TurnkeyAuth({ ownerAddress, turnkeyWalletClient }: { ownerAddress?: string | null; account?: any; turnkeyWalletClient?: any } = {}) {
77
+ const { passkeyClient, turnkey } = useTurnkey();
78
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
79
+ const [userName, setUserName] = useState('');
80
+ const [userEmail, setUserEmail] = useState('');
81
+ const [isLoading, setIsLoading] = useState(false);
82
+ const [error, setError] = useState<string | null>(null);
83
+ const [userSubOrgId, setUserSubOrgId] = useState<string | null>(null);
84
+ const [walletAddress, setWalletAddress] = useState<string | null>(null);
85
+ const [storedAccount, setStoredAccount] = useState<{ subOrgId: string; email: string; walletAddress: string } | null>(null);
86
+ const [showPasskeyForm, setShowPasskeyForm] = useState(false);
87
+ const [eoaEthBalance, setEoaEthBalance] = useState<string | null>(null);
88
+ const [eoaSbcBalance, setEoaSbcBalance] = useState<string | null>(null);
89
+ const [isLoadingEoaBalances, setIsLoadingEoaBalances] = useState(false);
90
+ const [showAccountSelector, setShowAccountSelector] = useState(false);
91
+ const [accountHistory, setAccountHistory] = useState<AccountHistoryItem[]>([]);
92
+
93
+ // IMMEDIATE auth check on mount - before Turnkey SDK loads
94
+ useEffect(() => {
95
+ const storedSubOrgId = localStorage.getItem('turnkey_sub_org_id');
96
+ const storedAuthType = localStorage.getItem('turnkey_auth_type');
97
+ const storedWalletAddress = localStorage.getItem('turnkey_wallet_address');
98
+ const walletReady = localStorage.getItem('turnkey_wallet_ready');
99
+
100
+ // If we have stored credentials (passkey OR wallet), load account data
101
+ if (storedSubOrgId && storedAuthType) {
102
+ console.log('⚡️ [FAST_AUTH] Found stored credentials, loading account data', {
103
+ authType: storedAuthType,
104
+ walletReady,
105
+ });
106
+ setUserSubOrgId(storedSubOrgId);
107
+ setWalletAddress(storedWalletAddress);
108
+
109
+ // Only set isAuthenticated=true if wallet is ready to auto-connect
110
+ // For wallet users: check if turnkey_wallet_ready flag exists
111
+ // For passkey users: will be set by checkAuth effect when Turnkey session loads
112
+ if (storedAuthType === 'wallet' && walletReady === 'true') {
113
+ setIsAuthenticated(true);
114
+ }
115
+
116
+ setIsLoading(false);
117
+
118
+ // Load email if available
119
+ const storedEmail = localStorage.getItem('turnkey_user_email');
120
+ if (storedEmail) {
121
+ setUserEmail(storedEmail);
122
+ }
123
+ }
124
+ }, []);
125
+
126
+ // Load account history from localStorage
127
+ useEffect(() => {
128
+ const historyJson = localStorage.getItem('turnkey_account_history');
129
+ if (historyJson) {
130
+ try {
131
+ const history = JSON.parse(historyJson) as AccountHistoryItem[];
132
+ setAccountHistory(history);
133
+ } catch (e) {
134
+ console.error('Failed to parse account history:', e);
135
+ setAccountHistory([]);
136
+ }
137
+ }
138
+ }, []);
139
+
140
+ // Helper: Add or update account in history
141
+ const saveAccountToHistory = (account: Omit<AccountHistoryItem, 'lastUsed'>) => {
142
+ const historyJson = localStorage.getItem('turnkey_account_history');
143
+ let history: AccountHistoryItem[] = [];
144
+
145
+ if (historyJson) {
146
+ try {
147
+ history = JSON.parse(historyJson);
148
+ } catch (e) {
149
+ history = [];
150
+ }
151
+ }
152
+
153
+ // Check if account already exists
154
+ const existingIndex = history.findIndex(h => h.subOrgId === account.subOrgId);
155
+ const newAccount: AccountHistoryItem = {
156
+ ...account,
157
+ lastUsed: Date.now(),
158
+ };
159
+
160
+ if (existingIndex >= 0) {
161
+ // Update existing account
162
+ history[existingIndex] = newAccount;
163
+ } else {
164
+ // Add new account
165
+ history.push(newAccount);
166
+ }
167
+
168
+ // Sort by lastUsed (most recent first)
169
+ history.sort((a, b) => b.lastUsed - a.lastUsed);
170
+
171
+ localStorage.setItem('turnkey_account_history', JSON.stringify(history));
172
+ setAccountHistory(history);
173
+ };
174
+
175
+ // Helper: Switch to a different account
176
+ const switchToAccount = (account: AccountHistoryItem) => {
177
+ localStorage.setItem('turnkey_sub_org_id', account.subOrgId);
178
+ localStorage.setItem('turnkey_auth_type', account.authType);
179
+ if (account.email) {
180
+ localStorage.setItem('turnkey_user_email', account.email);
181
+ }
182
+ if (account.walletAddress) {
183
+ localStorage.setItem('turnkey_wallet_address', account.walletAddress);
184
+ }
185
+
186
+ // Clear wallet ready flag when switching accounts (requires re-connection)
187
+ localStorage.removeItem('turnkey_wallet_ready');
188
+
189
+ // Update last used timestamp
190
+ saveAccountToHistory(account);
191
+
192
+ // Reload page to reinitialize with new account
193
+ window.location.reload();
194
+ };
195
+
196
+ // Fetch EOA balances when owner address is available
197
+ useEffect(() => {
198
+ if (!ownerAddress) {
199
+ setEoaEthBalance(null);
200
+ setEoaSbcBalance(null);
201
+ return;
202
+ }
203
+
204
+ const fetchEoaBalances = async () => {
205
+ setIsLoadingEoaBalances(true);
206
+ try {
207
+ // Fetch ETH balance
208
+ const ethBalance = await publicClient.getBalance({
209
+ address: ownerAddress as `0x${string}`,
210
+ });
211
+ setEoaEthBalance(ethBalance.toString());
212
+
213
+ // Fetch SBC balance
214
+ const sbcBalance = await publicClient.readContract({
215
+ address: SBC_TOKEN_ADDRESS(chain) as `0x${string}`,
216
+ abi: erc20Abi,
217
+ functionName: 'balanceOf',
218
+ args: [ownerAddress as `0x${string}`],
219
+ });
220
+ setEoaSbcBalance(sbcBalance.toString());
221
+ } catch (error) {
222
+ console.error('Failed to fetch EOA balances:', error);
223
+ setEoaEthBalance(null);
224
+ setEoaSbcBalance(null);
225
+ } finally {
226
+ setIsLoadingEoaBalances(false);
227
+ }
228
+ };
229
+
230
+ fetchEoaBalances();
231
+ }, [ownerAddress]);
232
+
233
+ useEffect(() => {
234
+ const checkAuth = async () => {
235
+ console.log('🔍 [AUTH] Checking authentication status...');
236
+
237
+ // Check localStorage for stored account
238
+ const storedSubOrgId = localStorage.getItem('turnkey_sub_org_id');
239
+ const storedEmail = localStorage.getItem('turnkey_user_email');
240
+ const storedWalletAddr = localStorage.getItem('turnkey_wallet_address');
241
+
242
+ if (storedSubOrgId) {
243
+ setStoredAccount({
244
+ subOrgId: storedSubOrgId,
245
+ email: storedEmail || 'Unknown',
246
+ walletAddress: storedWalletAddr || 'Unknown',
247
+ });
248
+ console.log('💾 [AUTH] Found stored account:', { storedSubOrgId, storedEmail });
249
+ }
250
+
251
+ if (turnkey) {
252
+ console.log('✅ [AUTH] Turnkey instance available');
253
+ try {
254
+ const session = await turnkey.getSession();
255
+ console.log('📋 [AUTH] Session:', session);
256
+
257
+ // Check if this is a passkey user (only they use Turnkey sessions)
258
+ const storedAuthType = localStorage.getItem('turnkey_auth_type');
259
+
260
+ if (session?.organizationId) {
261
+ setIsAuthenticated(true);
262
+ setUserSubOrgId(session.organizationId);
263
+ setIsLoading(false); // Make sure loading is false when auto-authenticated
264
+ console.log('✅ [AUTH] User is authenticated', { organizationId: session.organizationId });
265
+ } else if (storedAuthType === 'passkey') {
266
+ // Only reset isAuthenticated for passkey users who have no session
267
+ // Wallet users don't use Turnkey sessions, so don't reset their auth state
268
+ console.log('ℹ️ [AUTH] No active Turnkey session found (passkey user)');
269
+ setIsAuthenticated(false);
270
+ } else {
271
+ console.log('ℹ️ [AUTH] No Turnkey session needed (wallet user)');
272
+ }
273
+ } catch (err) {
274
+ console.error('❌ [AUTH] Error checking session:', err);
275
+ // Only reset for passkey users
276
+ const storedAuthType = localStorage.getItem('turnkey_auth_type');
277
+ if (storedAuthType === 'passkey') {
278
+ setIsAuthenticated(false);
279
+ }
280
+ }
281
+ } else {
282
+ console.log('⏳ [AUTH] Turnkey instance not ready yet');
283
+ }
284
+ };
285
+ checkAuth();
286
+ }, [turnkey]);
287
+
288
+ const handleSignup = async (e: React.FormEvent) => {
289
+ e.preventDefault();
290
+ if (!passkeyClient || !userName || !userEmail) return;
291
+
292
+ console.log('🚀 [SIGNUP] Starting signup flow...');
293
+ console.log('📝 [SIGNUP] User:', { userName, userEmail });
294
+
295
+ setIsLoading(true);
296
+ setError(null);
297
+
298
+ try {
299
+ // Step 1: Create passkey with WebAuthn
300
+ console.log('🔐 [SIGNUP] Step 1: Creating passkey with WebAuthn...');
301
+ console.log('📋 [SIGNUP] passkeyClient available:', !!passkeyClient);
302
+
303
+ if (!passkeyClient) {
304
+ throw new Error('Passkey client not initialized');
305
+ }
306
+
307
+ const passkeyResponse = await Promise.race([
308
+ passkeyClient.createUserPasskey({
309
+ publicKey: {
310
+ user: {
311
+ name: userEmail,
312
+ displayName: userName,
313
+ },
314
+ },
315
+ }),
316
+ new Promise((_, reject) =>
317
+ setTimeout(() => reject(new Error('Passkey creation timed out after 60s. Did you cancel the browser prompt?')), 60000)
318
+ )
319
+ ]);
320
+ console.log('✅ [SIGNUP] Step 1: Passkey created successfully!');
321
+ console.log('📦 [SIGNUP] Full passkey response:', passkeyResponse);
322
+
323
+ // Extract encodedChallenge and attestation from response
324
+ const { encodedChallenge, attestation } = passkeyResponse;
325
+ console.log('📦 [SIGNUP] Extracted data:', {
326
+ hasEncodedChallenge: !!encodedChallenge,
327
+ hasAttestation: !!attestation,
328
+ attestation,
329
+ });
330
+
331
+ // Step 2: Call our backend to create sub-org
332
+ const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3001';
333
+ console.log('🌐 [SIGNUP] Step 2: Calling backend at', backendUrl);
334
+ const response = await fetch(`${backendUrl}/api/create-sub-org`, {
335
+ method: 'POST',
336
+ headers: { 'Content-Type': 'application/json' },
337
+ body: JSON.stringify({
338
+ userName,
339
+ userEmail,
340
+ attestation,
341
+ challenge: encodedChallenge,
342
+ }),
343
+ });
344
+ console.log('📡 [SIGNUP] Backend response status:', response.status);
345
+
346
+ if (!response.ok) {
347
+ const errorData = await response.json();
348
+ console.error('❌ [SIGNUP] Backend error:', errorData);
349
+ throw new Error(errorData.error || 'Failed to create account');
350
+ }
351
+
352
+ const { subOrganizationId, addresses } = await response.json();
353
+ console.log('✅ [SIGNUP] Step 2: Sub-org and wallet created!', { subOrganizationId, addresses });
354
+
355
+ // Step 3: Authenticate to the sub-org with the passkey
356
+ console.log('🔐 [SIGNUP] Step 3: Authenticating to sub-org...');
357
+ await passkeyClient.login({
358
+ organizationId: subOrganizationId,
359
+ });
360
+ console.log('✅ [SIGNUP] Step 3: Authenticated to sub-org!');
361
+
362
+ const walletAddr = addresses[0];
363
+
364
+ // Store account info in localStorage
365
+ localStorage.setItem('turnkey_sub_org_id', subOrganizationId);
366
+ localStorage.setItem('turnkey_user_email', userEmail);
367
+ localStorage.setItem('turnkey_wallet_address', walletAddr);
368
+ localStorage.setItem('turnkey_auth_type', 'passkey'); // Mark as passkey auth
369
+
370
+ // Save to account history (NEVER LOSE THIS ACCOUNT!)
371
+ saveAccountToHistory({
372
+ subOrgId: subOrganizationId,
373
+ email: userEmail,
374
+ walletAddress: walletAddr,
375
+ authType: 'passkey',
376
+ });
377
+
378
+ setUserSubOrgId(subOrganizationId);
379
+ setWalletAddress(walletAddr);
380
+ setIsAuthenticated(true);
381
+
382
+ console.log('🎉 [SIGNUP] Signup complete! Reloading page...');
383
+
384
+ // Reload page to ensure clean state
385
+ setTimeout(() => {
386
+ window.location.reload();
387
+ }, 100);
388
+ } catch (err: any) {
389
+ console.error('❌ [SIGNUP] Error:', err);
390
+ console.error('❌ [SIGNUP] Error stack:', err.stack);
391
+ setError(err.message || 'Failed to create account');
392
+ } finally {
393
+ setIsLoading(false);
394
+ console.log('🏁 [SIGNUP] Signup flow ended');
395
+ }
396
+ };
397
+
398
+
399
+ const handleLogout = async () => {
400
+ try {
401
+ // Save current account to history before logging out (NEVER LOSE ACCOUNT!)
402
+ const currentSubOrgId = localStorage.getItem('turnkey_sub_org_id');
403
+ const currentEmail = localStorage.getItem('turnkey_user_email');
404
+ const currentWallet = localStorage.getItem('turnkey_wallet_address');
405
+ const currentAuthType = localStorage.getItem('turnkey_auth_type');
406
+
407
+ if (currentSubOrgId && currentAuthType) {
408
+ saveAccountToHistory({
409
+ subOrgId: currentSubOrgId,
410
+ email: currentEmail || undefined,
411
+ walletAddress: currentWallet || undefined,
412
+ authType: currentAuthType as 'passkey' | 'wallet',
413
+ });
414
+ }
415
+
416
+ await turnkey?.logout();
417
+
418
+ // Clear session-specific data but KEEP organizationId, walletAddress, and auth_type for fast re-login
419
+ // This allows quick re-authentication with the same method
420
+ localStorage.removeItem('turnkey_user_email');
421
+ localStorage.removeItem('turnkey_wallet_ready'); // Clear wallet ready flag
422
+ // Keep: turnkey_sub_org_id, turnkey_auth_type, turnkey_wallet_address
423
+
424
+ setIsAuthenticated(false);
425
+ setUserSubOrgId(null);
426
+ setWalletAddress(null);
427
+
428
+ console.log('🔓 [LOGOUT] Logged out (kept auth data for re-login), reloading page...');
429
+ // Reload page to clear all state
430
+ setTimeout(() => {
431
+ window.location.reload();
432
+ }, 100);
433
+ } catch (err) {
434
+ console.error('Logout error:', err);
435
+ }
436
+ };
437
+
438
+ const handleContinueWithPasskey = async () => {
439
+ // Prevent multiple simultaneous calls
440
+ if (isLoading) {
441
+ console.log('⏸️ [CONTINUE_PASSKEY] Already in progress, ignoring duplicate call');
442
+ return;
443
+ }
444
+
445
+ console.log('🚀 [CONTINUE_PASSKEY] Starting unified passkey flow...');
446
+
447
+ setIsLoading(true);
448
+ setError(null);
449
+
450
+ try {
451
+ // Check stored auth type - if user previously used wallet, clear that session
452
+ const storedAuthType = localStorage.getItem('turnkey_auth_type');
453
+ if (storedAuthType === 'wallet') {
454
+ console.log('🔄 [CONTINUE_PASSKEY] Switching from wallet to passkey - clearing old session');
455
+ localStorage.removeItem('turnkey_sub_org_id');
456
+ localStorage.removeItem('turnkey_wallet_address');
457
+ localStorage.removeItem('turnkey_auth_type');
458
+ }
459
+
460
+ // Check if we have a stored sub-org ID from previous signup/login
461
+ const storedSubOrgId = localStorage.getItem('turnkey_sub_org_id');
462
+ const storedWalletAddress = localStorage.getItem('turnkey_wallet_address');
463
+
464
+ console.log('📦 [CONTINUE_PASSKEY] Stored data:', { storedSubOrgId, storedWalletAddress });
465
+
466
+ if (storedSubOrgId) {
467
+ // Try to login with passkey for this organization
468
+ console.log('🔐 [CONTINUE_PASSKEY] Attempting passkey login for org:', storedSubOrgId);
469
+
470
+ try {
471
+ await passkeyClient!.login({
472
+ organizationId: storedSubOrgId,
473
+ });
474
+ console.log('✅ [CONTINUE_PASSKEY] Passkey authentication successful!');
475
+
476
+ // Fetch wallet address if we don't have it
477
+ let walletAddr = storedWalletAddress;
478
+ if (!walletAddr) {
479
+ console.log('💼 [CONTINUE_PASSKEY] Fetching wallet info...');
480
+ const wallets = await passkeyClient!.getWallets({
481
+ organizationId: storedSubOrgId,
482
+ });
483
+
484
+ const walletId = wallets?.wallets[0]?.walletId;
485
+ if (walletId) {
486
+ const accounts = await passkeyClient!.getWalletAccounts({
487
+ organizationId: storedSubOrgId,
488
+ walletId,
489
+ });
490
+ walletAddr = accounts?.accounts[0]?.address;
491
+ if (walletAddr) {
492
+ localStorage.setItem('turnkey_wallet_address', walletAddr);
493
+ }
494
+ }
495
+ }
496
+
497
+ // Ensure auth type is set for passkey users
498
+ localStorage.setItem('turnkey_auth_type', 'passkey');
499
+
500
+ // Save to account history
501
+ const storedEmail = localStorage.getItem('turnkey_user_email');
502
+ saveAccountToHistory({
503
+ subOrgId: storedSubOrgId,
504
+ email: storedEmail || undefined,
505
+ walletAddress: walletAddr || undefined,
506
+ authType: 'passkey',
507
+ });
508
+
509
+ console.log('🎯 [CONTINUE_PASSKEY] Setting authenticated state...');
510
+ setUserSubOrgId(storedSubOrgId);
511
+ setWalletAddress(walletAddr);
512
+ setIsAuthenticated(true);
513
+
514
+ console.log('🎉 [CONTINUE_PASSKEY] Login complete! Reloading page...');
515
+ setTimeout(() => {
516
+ window.location.reload();
517
+ }, 100);
518
+ return;
519
+ } catch (loginError: any) {
520
+ console.error('❌ [CONTINUE_PASSKEY] Passkey login failed:', loginError);
521
+ setError(loginError.message || 'Failed to authenticate with passkey. Please try again or create a new account.');
522
+ setIsLoading(false);
523
+ return;
524
+ }
525
+ }
526
+
527
+ // No stored account - show signup form
528
+ console.log('📝 [CONTINUE_PASSKEY] No stored account, showing signup form...');
529
+ setShowPasskeyForm(true);
530
+ } catch (err: any) {
531
+ console.error('❌ [CONTINUE_PASSKEY] Error:', err);
532
+ setError(err.message || 'Failed to continue with passkey');
533
+ } finally {
534
+ setIsLoading(false);
535
+ }
536
+ };
537
+
538
+ const handleConnectWallet = async () => {
539
+ if (typeof window === 'undefined' || !window.ethereum) {
540
+ setError('MetaMask not found. Please install MetaMask browser extension.');
541
+ return;
542
+ }
543
+
544
+ console.log('🚀 [CONNECT_WALLET] Starting unified wallet connection flow...');
545
+
546
+ setIsLoading(true);
547
+ setError(null);
548
+
549
+ try {
550
+ // Check stored auth type - if switching from passkey, clear that session
551
+ const storedAuthType = localStorage.getItem('turnkey_auth_type');
552
+ if (storedAuthType === 'passkey') {
553
+ console.log('🔄 [CONNECT_WALLET] Switching from passkey to wallet - clearing old session');
554
+ localStorage.removeItem('turnkey_sub_org_id');
555
+ localStorage.removeItem('turnkey_wallet_address');
556
+ localStorage.removeItem('turnkey_auth_type');
557
+ } else if (storedAuthType === 'wallet') {
558
+ console.log('🔄 [CONNECT_WALLET] Re-connecting wallet - clearing old session for fresh connection');
559
+ // Clear to allow connecting different wallet or re-authenticating
560
+ localStorage.removeItem('turnkey_sub_org_id');
561
+ localStorage.removeItem('turnkey_wallet_address');
562
+ localStorage.removeItem('turnkey_auth_type');
563
+ }
564
+
565
+ const ethereum = window.ethereum!;
566
+
567
+ // Step 1: Connect to MetaMask and get account
568
+ console.log('🔐 [CONNECT_WALLET] Step 1: Connecting to MetaMask...');
569
+ const accounts = await ethereum.request({ method: 'eth_requestAccounts' }) as string[];
570
+ const address = accounts[0];
571
+ console.log('✅ [CONNECT_WALLET] Connected to address:', address);
572
+
573
+ // Step 2: Request signature to derive public key
574
+ console.log('🔑 [CONNECT_WALLET] Step 2: Requesting signature to derive public key...');
575
+ const message = 'Sign this message to authenticate with Turnkey';
576
+ const signature = await (ethereum.request as any)({
577
+ method: 'personal_sign',
578
+ params: [message, address],
579
+ }) as string;
580
+ console.log('✅ [CONNECT_WALLET] Signature received');
581
+
582
+ // Step 3: Recover public key from signature
583
+ console.log('🔑 [CONNECT_WALLET] Step 3: Recovering public key from signature...');
584
+ const { recoverPublicKey, hashMessage } = await import('viem');
585
+ const messageHash = hashMessage(message);
586
+ const uncompressedPublicKey = await recoverPublicKey({
587
+ hash: messageHash,
588
+ signature: signature as `0x${string}`,
589
+ });
590
+ console.log('✅ [CONNECT_WALLET] Uncompressed public key:', uncompressedPublicKey);
591
+
592
+ // Remove 0x prefix for Turnkey (it expects keys without prefix)
593
+ const publicKeyWithoutPrefix = uncompressedPublicKey.startsWith('0x')
594
+ ? uncompressedPublicKey.slice(2)
595
+ : uncompressedPublicKey;
596
+ console.log('✅ [CONNECT_WALLET] Public key (without 0x):', publicKeyWithoutPrefix);
597
+
598
+ const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3001';
599
+
600
+ // Step 4: Try to find existing sub-org (LOGIN ATTEMPT)
601
+ console.log('🔍 [CONNECT_WALLET] Step 4: Checking for existing account...');
602
+ const loginResponse = await fetch(`${backendUrl}/api/get-sub-org-by-wallet`, {
603
+ method: 'POST',
604
+ headers: { 'Content-Type': 'application/json' },
605
+ body: JSON.stringify({ publicKey: publicKeyWithoutPrefix }),
606
+ });
607
+
608
+ if (!loginResponse.ok) {
609
+ throw new Error('Failed to check for existing account');
610
+ }
611
+
612
+ const loginData = await loginResponse.json();
613
+ console.log('📦 [CONNECT_WALLET] Login check response:', loginData);
614
+
615
+ let subOrganizationId: string;
616
+ let walletAddr: string;
617
+
618
+ // Check if existing sub-org found AND if the wallet address matches the connected wallet
619
+ const hasExistingSubOrg = loginData.subOrganizationId && loginData.addresses?.length > 0;
620
+ const existingWalletMatches = hasExistingSubOrg &&
621
+ loginData.addresses[0]?.toLowerCase() === address.toLowerCase();
622
+
623
+ if (hasExistingSubOrg && existingWalletMatches) {
624
+ // EXISTING USER - LOGIN (with matching wallet address)
625
+ console.log('✅ [CONNECT_WALLET] Found existing account with matching wallet! Logging in...');
626
+ subOrganizationId = loginData.subOrganizationId;
627
+ walletAddr = loginData.addresses[0];
628
+ } else {
629
+ // NEW USER - SIGNUP (or old sub-org with Turnkey wallet that doesn't match)
630
+ if (hasExistingSubOrg) {
631
+ console.log('⚠️ [CONNECT_WALLET] Found old sub-org but wallet address doesn\'t match. Creating new account...');
632
+ console.log(` Expected: ${address}, Found: ${loginData.addresses[0]}`);
633
+ } else {
634
+ console.log('🆕 [CONNECT_WALLET] No existing account found. Creating new account...');
635
+ }
636
+
637
+ // Use wallet address as username and create placeholder email
638
+ const walletUserName = `Wallet ${address.slice(0, 6)}...${address.slice(-4)}`;
639
+ const walletEmail = `${address.toLowerCase()}@wallet.turnkey`;
640
+
641
+ const signupResponse = await fetch(`${backendUrl}/api/create-sub-org-with-wallet`, {
642
+ method: 'POST',
643
+ headers: { 'Content-Type': 'application/json' },
644
+ body: JSON.stringify({
645
+ userName: walletUserName,
646
+ userEmail: walletEmail,
647
+ publicKey: publicKeyWithoutPrefix,
648
+ walletAddress: address,
649
+ }),
650
+ });
651
+
652
+ if (!signupResponse.ok) {
653
+ const errorData = await signupResponse.json();
654
+ throw new Error(errorData.error || 'Failed to create account');
655
+ }
656
+
657
+ const signupData = await signupResponse.json();
658
+ console.log('✅ [CONNECT_WALLET] New account created!', signupData);
659
+
660
+ subOrganizationId = signupData.subOrganizationId;
661
+ walletAddr = signupData.addresses[0];
662
+
663
+ // Store email for new users
664
+ localStorage.setItem('turnkey_user_email', walletEmail);
665
+ }
666
+
667
+ // Step 5: Store account info and set state
668
+ console.log('💾 [CONNECT_WALLET] Step 5: Storing account info...');
669
+ localStorage.setItem('turnkey_sub_org_id', subOrganizationId);
670
+ localStorage.setItem('turnkey_wallet_address', walletAddr);
671
+ localStorage.setItem('turnkey_auth_type', 'wallet'); // Mark as wallet auth
672
+
673
+ // Save to account history
674
+ const walletEmail = localStorage.getItem('turnkey_user_email');
675
+ saveAccountToHistory({
676
+ subOrgId: subOrganizationId,
677
+ email: walletEmail || undefined,
678
+ walletAddress: walletAddr,
679
+ authType: 'wallet',
680
+ });
681
+
682
+ setUserSubOrgId(subOrganizationId);
683
+ setWalletAddress(walletAddr);
684
+ setIsAuthenticated(true);
685
+
686
+ // Set flag to indicate wallet client should be created after reload
687
+ localStorage.setItem('turnkey_wallet_ready', 'true');
688
+
689
+ console.log('🎉 [CONNECT_WALLET] Wallet connection complete! Reloading page...');
690
+ setTimeout(() => {
691
+ window.location.reload();
692
+ }, 100);
693
+ } catch (err: any) {
694
+ console.error('❌ [CONNECT_WALLET] Error:', err);
695
+ console.error('❌ [CONNECT_WALLET] Error stack:', err.stack);
696
+ setError(err.message || 'Failed to connect wallet');
697
+ } finally {
698
+ setIsLoading(false);
699
+ console.log('🏁 [CONNECT_WALLET] Wallet connection flow ended');
700
+ }
701
+ };
702
+
703
+
704
+ const handleShowAccountSelector = () => {
705
+ setShowAccountSelector(true);
706
+ };
707
+
708
+ console.log('🎨 [RENDER] TurnkeyAuth render state:', {
709
+ isAuthenticated,
710
+ isLoading,
711
+ userSubOrgId,
712
+ walletAddress,
713
+ storedAccount,
714
+ });
715
+
716
+ if (!isAuthenticated) {
717
+ return (
718
+ <div className="mb-6 p-6 bg-white border border-gray-200 rounded-lg shadow-sm">
719
+ <h3 className="font-semibold text-gray-800 mb-4 text-lg">🔐 Turnkey Authentication</h3>
720
+
721
+ {/* Account Selector Modal */}
722
+ {showAccountSelector && accountHistory.length > 0 && (
723
+ <div className="mb-4 p-4 bg-gray-50 border border-gray-300 rounded-lg">
724
+ <div className="flex justify-between items-center mb-3">
725
+ <h4 className="font-medium text-gray-800">Select Account</h4>
726
+ <button
727
+ onClick={() => setShowAccountSelector(false)}
728
+ className="text-xs text-gray-600 hover:text-gray-800"
729
+ >
730
+ ✕ Cancel
731
+ </button>
732
+ </div>
733
+ <div className="space-y-2 max-h-60 overflow-y-auto">
734
+ {accountHistory.map((acc) => (
735
+ <button
736
+ key={acc.subOrgId}
737
+ onClick={() => switchToAccount(acc)}
738
+ className="w-full p-3 bg-white border border-gray-200 rounded hover:bg-blue-50 hover:border-blue-300 text-left transition"
739
+ >
740
+ <div className="flex justify-between items-start">
741
+ <div className="flex-1">
742
+ <p className="text-sm font-medium text-gray-800">
743
+ {acc.email || `${acc.authType === 'passkey' ? '🔐 Passkey' : '👛 Wallet'} Account`}
744
+ </p>
745
+ {acc.walletAddress && (
746
+ <p className="text-xs text-gray-600 font-mono mt-1">{acc.walletAddress.slice(0, 10)}...{acc.walletAddress.slice(-8)}</p>
747
+ )}
748
+ <p className="text-xs text-gray-500 mt-1">
749
+ Last used: {new Date(acc.lastUsed).toLocaleDateString()}
750
+ </p>
751
+ </div>
752
+ {storedAccount?.subOrgId === acc.subOrgId && (
753
+ <span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">Current</span>
754
+ )}
755
+ </div>
756
+ </button>
757
+ ))}
758
+ </div>
759
+ <div className="mt-3 pt-3 border-t border-gray-300">
760
+ <button
761
+ onClick={() => {
762
+ // Allow creating a new account by just closing the selector
763
+ // and letting them use the normal signup flow
764
+ setShowAccountSelector(false);
765
+ setShowPasskeyForm(true);
766
+ }}
767
+ className="w-full text-sm text-blue-600 hover:text-blue-800 font-medium"
768
+ >
769
+ + Create New Account
770
+ </button>
771
+ </div>
772
+ </div>
773
+ )}
774
+
775
+ {/* Stored Account Info - shown when not selecting accounts */}
776
+ {storedAccount && !showAccountSelector && accountHistory.length > 1 && (
777
+ <div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded">
778
+ <div className="flex justify-between items-start">
779
+ <div className="flex-1">
780
+ <p className="text-sm font-medium text-blue-800 mb-1">Current Account</p>
781
+ <p className="text-xs text-blue-600 mb-1">
782
+ {storedAccount.email !== 'Unknown' ? `Email: ${storedAccount.email}` : 'Passkey Account'}
783
+ </p>
784
+ <p className="text-xs text-blue-600 font-mono break-all">ID: {storedAccount.subOrgId.slice(0, 20)}...</p>
785
+ </div>
786
+ <button
787
+ onClick={handleShowAccountSelector}
788
+ className="ml-2 text-xs bg-blue-600 text-white px-2 py-1 rounded hover:bg-blue-700"
789
+ title="Switch to a different account"
790
+ >
791
+ Switch ({accountHistory.length})
792
+ </button>
793
+ </div>
794
+ </div>
795
+ )}
796
+
797
+ {error && !showAccountSelector && (
798
+ <div className="mb-4 p-3 bg-red-50 border border-red-200 rounded text-sm text-red-600">
799
+ {error}
800
+ </div>
801
+ )}
802
+
803
+ {!showAccountSelector && (
804
+ <div className="space-y-6">
805
+ {/* Unified Passkey Flow */}
806
+ <div>
807
+ <h4 className="font-medium text-gray-700 mb-3">Continue with Passkey</h4>
808
+
809
+ {!showPasskeyForm ? (
810
+ // Initial state - just show the Continue button
811
+ <>
812
+ <button
813
+ onClick={handleContinueWithPasskey}
814
+ disabled={isLoading}
815
+ className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
816
+ >
817
+ {isLoading ? 'Checking...' : 'Continue with Passkey'}
818
+ </button>
819
+ <p className="mt-2 text-xs text-gray-500">
820
+ Uses biometric authentication (Face ID, Touch ID, Windows Hello)
821
+ </p>
822
+ </>
823
+ ) : (
824
+ // Show signup form if needed
825
+ <>
826
+ <p className="text-sm text-gray-600 mb-3">Let's create your account</p>
827
+ <form onSubmit={handleSignup} className="space-y-3">
828
+ <input
829
+ type="text"
830
+ value={userName}
831
+ onChange={(e) => setUserName(e.target.value)}
832
+ placeholder="Your name"
833
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
834
+ required
835
+ autoFocus
836
+ />
837
+ <input
838
+ type="email"
839
+ value={userEmail}
840
+ onChange={(e) => setUserEmail(e.target.value)}
841
+ placeholder="your@email.com"
842
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
843
+ required
844
+ />
845
+ <button
846
+ type="submit"
847
+ disabled={isLoading || !userName || !userEmail}
848
+ className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
849
+ >
850
+ {isLoading ? 'Creating Account...' : 'Create Account'}
851
+ </button>
852
+ </form>
853
+ <button
854
+ onClick={() => setShowPasskeyForm(false)}
855
+ className="mt-2 text-sm text-gray-600 hover:text-gray-800"
856
+ >
857
+ ← Back
858
+ </button>
859
+ </>
860
+ )}
861
+ </div>
862
+
863
+ {/* Divider */}
864
+ <div className="relative">
865
+ <div className="absolute inset-0 flex items-center">
866
+ <div className="w-full border-t border-gray-300"></div>
867
+ </div>
868
+ <div className="relative flex justify-center text-sm">
869
+ <span className="px-2 bg-white text-gray-500">OR</span>
870
+ </div>
871
+ </div>
872
+
873
+ {/* Wallet Connect (Unified Login/Signup) */}
874
+ <div>
875
+ <h4 className="font-medium text-gray-700 mb-3">Connect with Wallet</h4>
876
+ <button
877
+ onClick={handleConnectWallet}
878
+ disabled={isLoading}
879
+ className="w-full bg-purple-600 text-white py-2 px-4 rounded-md hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed"
880
+ >
881
+ {isLoading ? 'Connecting...' : 'Connect Wallet'}
882
+ </button>
883
+ <p className="mt-2 text-xs text-gray-500">
884
+ 💡 Works across devices with MetaMask/Coinbase • No email required • Auto creates account if needed
885
+ </p>
886
+ </div>
887
+
888
+ </div>
889
+ )}
890
+
891
+ {!showAccountSelector && (
892
+ <p className="mt-4 text-xs text-gray-500 text-center">
893
+ Choose your preferred authentication method above
894
+ </p>
895
+ )}
896
+ </div>
897
+ );
898
+ }
899
+
900
+ const formatEthBalance = (balance: string | null): string => {
901
+ if (!balance) return '0.0000';
902
+ try {
903
+ return (Number(balance) / 1e18).toFixed(4);
904
+ } catch {
905
+ return '0.0000';
906
+ }
907
+ };
908
+
909
+ const formatSbcBalance = (balance: string | null): string => {
910
+ if (!balance) return '0.00';
911
+ try {
912
+ return (Number(balance) / Math.pow(10, SBC_DECIMALS(chain))).toFixed(2);
913
+ } catch {
914
+ return '0.00';
915
+ }
916
+ };
917
+
918
+ // Use ownerAddress from props if available, otherwise fallback to local walletAddress
919
+ const displayAddress = ownerAddress || walletAddress;
920
+
921
+ return (
922
+ <div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
923
+ <div className="flex justify-between items-start">
924
+ <div className="flex-1">
925
+ <h3 className="font-semibold text-green-800 mb-2">
926
+ {turnkeyWalletClient ? '✅ Authenticated' : '🔐 Re-authentication Required'}
927
+ </h3>
928
+ <div className="space-y-1 text-xs">
929
+ <p className="text-green-600">Sub-Organization: {userSubOrgId}</p>
930
+ {userEmail && (
931
+ <p className="text-green-600">Owner (Email): {userEmail}</p>
932
+ )}
933
+ {displayAddress && (
934
+ <p className="text-green-600 font-mono">Owner (Turnkey): {displayAddress}</p>
935
+ )}
936
+ {ownerAddress && (
937
+ <>
938
+ <p className="text-green-600">
939
+ ETH Balance: {isLoadingEoaBalances ? 'Loading...' : `${formatEthBalance(eoaEthBalance)} ETH`}
940
+ </p>
941
+ <p className="text-green-600">
942
+ SBC Balance: {isLoadingEoaBalances ? 'Loading...' : `${formatSbcBalance(eoaSbcBalance)} SBC`}
943
+ </p>
944
+ </>
945
+ )}
946
+ {!ownerAddress && displayAddress && (
947
+ <p className="text-green-600 text-xs italic">⏳ Loading balances and smart account...</p>
948
+ )}
949
+ </div>
950
+ </div>
951
+ <button
952
+ onClick={handleLogout}
953
+ className="text-xs bg-green-600 text-white px-3 py-1 rounded hover:bg-green-700"
954
+ >
955
+ Logout
956
+ </button>
957
+ </div>
958
+ </div>
959
+ );
960
+ }
961
+
962
+ function SmartAccountInfo({
963
+ account,
964
+ isInitialized,
965
+ refreshAccount,
966
+ isLoadingAccount,
967
+ }: {
968
+ account: any;
969
+ isInitialized: boolean;
970
+ refreshAccount: () => Promise<void>;
971
+ isLoadingAccount: boolean;
972
+ }) {
973
+ const [isRefreshing, setIsRefreshing] = useState(false);
974
+ const [sbcBalance, setSbcBalance] = useState<string | null>(null);
975
+ const [isLoadingBalance, setIsLoadingBalance] = useState(false);
976
+
977
+ // Fetch SBC balance for smart account
978
+ useEffect(() => {
979
+ if (!account?.address) return;
980
+
981
+ const fetchSbcBalance = async () => {
982
+ setIsLoadingBalance(true);
983
+ try {
984
+ const balance = await publicClient.readContract({
985
+ address: SBC_TOKEN_ADDRESS(chain) as `0x${string}`,
986
+ abi: erc20Abi,
987
+ functionName: 'balanceOf',
988
+ args: [account.address as `0x${string}`],
989
+ });
990
+ setSbcBalance(balance.toString());
991
+ } catch (error) {
992
+ console.error('Failed to fetch smart account SBC balance:', error);
993
+ setSbcBalance('0');
994
+ } finally {
995
+ setIsLoadingBalance(false);
996
+ }
997
+ };
998
+
999
+ fetchSbcBalance();
1000
+ }, [account?.address]);
1001
+
1002
+ const handleRefresh = async () => {
1003
+ setIsRefreshing(true);
1004
+ try {
1005
+ await refreshAccount?.();
1006
+ // Refresh SBC balance
1007
+ if (account?.address) {
1008
+ setIsLoadingBalance(true);
1009
+ try {
1010
+ const balance = await publicClient.readContract({
1011
+ address: SBC_TOKEN_ADDRESS(chain) as `0x${string}`,
1012
+ abi: erc20Abi,
1013
+ functionName: 'balanceOf',
1014
+ args: [account.address as `0x${string}`],
1015
+ });
1016
+ setSbcBalance(balance.toString());
1017
+ } catch (error) {
1018
+ console.error('Failed to refresh smart account SBC balance:', error);
1019
+ } finally {
1020
+ setIsLoadingBalance(false);
1021
+ }
1022
+ }
1023
+ } catch (error) {
1024
+ // error handled
1025
+ } finally {
1026
+ setIsRefreshing(false);
1027
+ }
1028
+ };
1029
+
1030
+ const formatEthBalance = (balance: string | null): string => {
1031
+ if (!balance) return '0.0000';
1032
+ try {
1033
+ return (Number(balance) / 1e18).toFixed(4);
1034
+ } catch {
1035
+ return '0.0000';
1036
+ }
1037
+ };
1038
+
1039
+ const formatSbcBalance = (balance: string | null): string => {
1040
+ if (!balance) return '0.00';
1041
+ try {
1042
+ return (Number(balance) / Math.pow(10, SBC_DECIMALS(chain))).toFixed(2);
1043
+ } catch {
1044
+ return '0.00';
1045
+ }
1046
+ };
1047
+
1048
+ if (!isInitialized || !account) {
1049
+ return null;
1050
+ }
1051
+
1052
+ return (
1053
+ <div className="mb-6 p-4 bg-purple-50 border border-purple-200 rounded-lg">
1054
+ <div className="flex justify-between items-center mb-2">
1055
+ <h3 className="font-semibold text-purple-800">🔐 Smart Account Status</h3>
1056
+ <button
1057
+ onClick={handleRefresh}
1058
+ disabled={isRefreshing || isLoadingAccount}
1059
+ className="text-xs bg-purple-600 text-white px-3 py-1 rounded hover:bg-purple-700 disabled:opacity-50"
1060
+ >
1061
+ {isRefreshing || isLoadingAccount ? '🔄 Refreshing...' : '🔄 Refresh'}
1062
+ </button>
1063
+ </div>
1064
+ <div className="space-y-2 text-sm">
1065
+ <div className="flex justify-between">
1066
+ <span className="text-purple-700">Smart Account:</span>
1067
+ <span className="font-mono text-xs text-purple-600">{account.address}</span>
1068
+ </div>
1069
+ <div className="flex justify-between">
1070
+ <span className="text-purple-700">Deployed:</span>
1071
+ <span className="text-purple-600">{account.isDeployed ? '✅ Yes' : '⏳ On first transaction'}</span>
1072
+ </div>
1073
+ <div className="flex justify-between">
1074
+ <span className="text-purple-700">Nonce:</span>
1075
+ <span className="text-purple-600">{account.nonce}</span>
1076
+ </div>
1077
+ <div className="pt-2 border-t border-purple-200">
1078
+ <p className="text-xs font-medium text-purple-700 mb-2">Balances:</p>
1079
+ <div className="space-y-1">
1080
+ <div className="flex justify-between">
1081
+ <span className="text-purple-700">ETH:</span>
1082
+ <span className="text-purple-600 font-mono text-xs">{formatEthBalance(account.balance)} ETH</span>
1083
+ </div>
1084
+ <div className="flex justify-between">
1085
+ <span className="text-purple-700">SBC:</span>
1086
+ <span className="text-purple-600 font-mono text-xs">
1087
+ {isLoadingBalance ? 'Loading...' : `${formatSbcBalance(sbcBalance)} SBC`}
1088
+ </span>
1089
+ </div>
1090
+ </div>
1091
+ </div>
1092
+ </div>
1093
+ </div>
1094
+ );
1095
+ }
1096
+
1097
+ function TransactionForm({ sbcAppKit, account, ownerAddress }: { sbcAppKit: any; account: any; ownerAddress: string | null }) {
1098
+ console.log('[TransactionForm] Render check:', {
1099
+ hasSbcAppKit: !!sbcAppKit,
1100
+ hasAccount: !!account,
1101
+ account: account,
1102
+ ownerAddress,
1103
+ });
1104
+
1105
+ const [recipient, setRecipient] = useState('');
1106
+ const [amount, setAmount] = useState('1');
1107
+ const [isLoading, setIsLoading] = useState(false);
1108
+ const [isSuccess, setIsSuccess] = useState(false);
1109
+ const [isError, setIsError] = useState(false);
1110
+ const [error, setError] = useState<Error | null>(null);
1111
+ const [transactionHash, setTransactionHash] = useState<string | null>(null);
1112
+ const walletClient = (sbcAppKit as any)?.walletClient;
1113
+ const isFormValid = recipient && /^0x[a-fA-F0-9]{40}$/.test(recipient) && parseFloat(amount) > 0;
1114
+
1115
+ const handleSendTransaction = async () => {
1116
+ if (!account || !ownerAddress || !walletClient) return;
1117
+
1118
+ setIsLoading(true);
1119
+ setIsSuccess(false);
1120
+ setIsError(false);
1121
+ setError(null);
1122
+ setTransactionHash(null);
1123
+
1124
+ try {
1125
+ const ownerChecksum = getAddress(ownerAddress);
1126
+ const spenderChecksum = getAddress(account.address);
1127
+ const value = parseUnits(amount, SBC_DECIMALS(chain));
1128
+ const deadline = Math.floor(Date.now() / 1000) + 60 * 30;
1129
+
1130
+ console.log('🔐 Requesting permit signature...');
1131
+ const signature = await getPermitSignature({
1132
+ publicClient: publicClient as PublicClient,
1133
+ walletClient: walletClient as WalletClient,
1134
+ owner: ownerChecksum,
1135
+ spender: spenderChecksum,
1136
+ value,
1137
+ tokenAddress: SBC_TOKEN_ADDRESS(chain),
1138
+ chainId: chain.id,
1139
+ deadline,
1140
+ });
1141
+
1142
+ if (!signature) {
1143
+ throw new Error('Failed to get permit signature');
1144
+ }
1145
+ const { r, s, v } = parseSignature(signature);
1146
+
1147
+ const permitCallData = encodeFunctionData({
1148
+ abi: permitAbi,
1149
+ functionName: 'permit',
1150
+ args: [ownerChecksum, spenderChecksum, value, deadline, v, r, s],
1151
+ });
1152
+ const transferFromCallData = encodeFunctionData({
1153
+ abi: erc20PermitAbi,
1154
+ functionName: 'transferFrom',
1155
+ args: [ownerChecksum, recipient as `0x${string}`, value],
1156
+ });
1157
+
1158
+ console.log('📤 Sending user operation...');
1159
+ const result = await sbcAppKit.sendUserOperation({
1160
+ calls: [
1161
+ { to: SBC_TOKEN_ADDRESS(chain) as `0x${string}`, data: permitCallData },
1162
+ { to: SBC_TOKEN_ADDRESS(chain) as `0x${string}`, data: transferFromCallData },
1163
+ ],
1164
+ });
1165
+
1166
+ console.log('✅ Transaction successful:', result);
1167
+ setIsSuccess(true);
1168
+ setTransactionHash(result.transactionHash);
1169
+ } catch (err) {
1170
+ console.error('❌ Transaction failed:', err);
1171
+ setIsError(true);
1172
+ setError(err instanceof Error ? err : new Error('Transaction failed'));
1173
+ } finally {
1174
+ setIsLoading(false);
1175
+ }
1176
+ };
1177
+
1178
+ if (!account) return null;
1179
+
1180
+ return (
1181
+ <div className="p-4 bg-white border border-gray-200 rounded-lg shadow-sm">
1182
+ <h3 className="font-semibold text-gray-800 mb-4">💸 Send SBC Tokens</h3>
1183
+ <div className="space-y-4">
1184
+ <div>
1185
+ <label className="block text-sm font-medium text-gray-700 mb-2">Recipient Address</label>
1186
+ <input
1187
+ type="text"
1188
+ value={recipient}
1189
+ onChange={(e) => setRecipient(e.target.value)}
1190
+ placeholder="0x..."
1191
+ className={`w-full px-3 py-2 text-xs font-mono border rounded-md focus:outline-none focus:ring-2 ${
1192
+ recipient && !isFormValid ? 'border-red-300 focus:ring-red-500' : 'border-gray-300 focus:ring-blue-500'
1193
+ }`}
1194
+ />
1195
+ {recipient && !/^0x[a-fA-F0-9]{40}$/.test(recipient) && (
1196
+ <p className="text-xs text-red-600 mt-1">Invalid Ethereum address</p>
1197
+ )}
1198
+ </div>
1199
+ <div>
1200
+ <label className="block text-sm font-medium text-gray-700 mb-2">Amount (SBC)</label>
1201
+ <input
1202
+ type="number"
1203
+ value={amount}
1204
+ onChange={(e) => setAmount(e.target.value)}
1205
+ placeholder="1.0"
1206
+ step="0.000001"
1207
+ min="0"
1208
+ className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
1209
+ />
1210
+ </div>
1211
+ <div className="p-3 bg-gray-50 rounded">
1212
+ <div className="flex justify-between text-sm">
1213
+ <span>Amount:</span>
1214
+ <span className="font-medium">{amount} SBC</span>
1215
+ </div>
1216
+ <div className="flex justify-between text-xs text-gray-600">
1217
+ <span>Gas fees:</span>
1218
+ <span>Covered by SBC Paymaster ✨</span>
1219
+ </div>
1220
+ </div>
1221
+ <button
1222
+ onClick={handleSendTransaction}
1223
+ disabled={!isFormValid || isLoading || !account || !ownerAddress || !walletClient}
1224
+ className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
1225
+ >
1226
+ {isLoading ? 'Waiting for signature...' : `Send ${amount} SBC`}
1227
+ </button>
1228
+ {isSuccess && transactionHash && (
1229
+ <div className="p-3 bg-green-50 border border-green-200 rounded">
1230
+ <p className="text-sm text-green-800 font-medium">✅ Transaction Successful!</p>
1231
+ <p className="text-xs text-green-600 font-mono break-all mt-1">
1232
+ <a
1233
+ href={`${chainExplorer(chain)}/tx/${transactionHash}`}
1234
+ target="_blank"
1235
+ rel="noopener noreferrer"
1236
+ className="hover:underline"
1237
+ >
1238
+ View on Explorer: {transactionHash}
1239
+ </a>
1240
+ </p>
1241
+ </div>
1242
+ )}
1243
+ {isError && error && (
1244
+ <div className="p-3 bg-red-50 border border-red-200 rounded">
1245
+ <p className="text-sm text-red-800 font-medium">❌ Transaction Failed</p>
1246
+ <p className="text-xs text-red-600 mt-1">{error.message}</p>
1247
+ </div>
1248
+ )}
1249
+ </div>
1250
+ </div>
1251
+ );
1252
+ }
1253
+
1254
+ function TurnkeyIntegration() {
1255
+ const { turnkey, passkeyClient } = useTurnkey();
1256
+ const [turnkeyClient, setTurnkeyClient] = useState<any>(null);
1257
+ const [organizationId, setOrganizationId] = useState('');
1258
+ const [turnkeyWalletClient, setTurnkeyWalletClient] = useState<any>(null);
1259
+ const [walletAddress, setWalletAddress] = useState<string | null>(null);
1260
+
1261
+ // INSTANT INITIALIZATION - Read from localStorage immediately on mount
1262
+ useEffect(() => {
1263
+ const storedSubOrgId = localStorage.getItem('turnkey_sub_org_id');
1264
+ const storedWalletAddress = localStorage.getItem('turnkey_wallet_address');
1265
+ const storedAuthType = localStorage.getItem('turnkey_auth_type');
1266
+
1267
+ if (storedSubOrgId && storedWalletAddress && storedAuthType === 'passkey') {
1268
+ console.log('⚡️ [TurnkeyIntegration FAST_INIT] Found passkey credentials, setting state immediately');
1269
+ setOrganizationId(storedSubOrgId);
1270
+ setWalletAddress(storedWalletAddress);
1271
+ // Don't set turnkeyClient yet - wait for SDK to load in the other useEffect
1272
+ }
1273
+ }, []);
1274
+
1275
+ useEffect(() => {
1276
+ const checkAuthAndSetup = async () => {
1277
+ // Check for stored authentication data
1278
+ const storedSubOrgId = localStorage.getItem('turnkey_sub_org_id');
1279
+ const storedWalletAddress = localStorage.getItem('turnkey_wallet_address');
1280
+ const storedAuthType = localStorage.getItem('turnkey_auth_type');
1281
+
1282
+ console.log('[TurnkeyIntegration] Checking authentication:', {
1283
+ storedSubOrgId,
1284
+ storedWalletAddress,
1285
+ storedAuthType,
1286
+ });
1287
+
1288
+ // If we have stored credentials, use auth_type flag to determine flow
1289
+ if (storedSubOrgId && storedWalletAddress && storedAuthType) {
1290
+ if (storedAuthType === 'wallet') {
1291
+ // WALLET AUTHENTICATION FLOW
1292
+ console.log('[TurnkeyIntegration] Using wallet authentication (MetaMask/Coinbase)');
1293
+ setOrganizationId(storedSubOrgId);
1294
+ setTurnkeyClient(null); // No passkey client for wallet users
1295
+ setWalletAddress(storedWalletAddress);
1296
+ return;
1297
+ } else if (storedAuthType === 'passkey') {
1298
+ // PASSKEY AUTHENTICATION FLOW
1299
+ console.log('[TurnkeyIntegration] Using passkey authentication');
1300
+
1301
+ if (!turnkey || !passkeyClient) {
1302
+ console.log('[TurnkeyIntegration] Waiting for Turnkey SDK to initialize...');
1303
+ setTurnkeyClient(null);
1304
+ // Don't clear organizationId and walletAddress - they may have been set by instant init
1305
+ // We'll update them when Turnkey SDK is ready
1306
+ return;
1307
+ }
1308
+
1309
+ try {
1310
+ const session = await turnkey.getSession();
1311
+ if (session?.organizationId) {
1312
+ console.log('[TurnkeyIntegration] Passkey session active:', session.organizationId);
1313
+ setOrganizationId(session.organizationId);
1314
+ setTurnkeyClient(passkeyClient);
1315
+ setWalletAddress(storedWalletAddress);
1316
+ return;
1317
+ } else {
1318
+ console.log('[TurnkeyIntegration] No active passkey session found');
1319
+ setTurnkeyClient(null);
1320
+ setOrganizationId('');
1321
+ setWalletAddress(null);
1322
+ return;
1323
+ }
1324
+ } catch (err) {
1325
+ console.error('[TurnkeyIntegration] Failed to get passkey session:', err);
1326
+ setTurnkeyClient(null);
1327
+ setOrganizationId('');
1328
+ setWalletAddress(null);
1329
+ return;
1330
+ }
1331
+ }
1332
+ }
1333
+
1334
+ // FALLBACK: Old logic for backwards compatibility (no auth_type flag)
1335
+ // This handles existing users who authenticated before the auth_type flag was added
1336
+ if (storedSubOrgId && storedWalletAddress && !storedAuthType) {
1337
+ console.log('[TurnkeyIntegration] No auth_type flag found, using legacy detection...');
1338
+
1339
+ // Try to detect based on Turnkey session
1340
+ if (turnkey && passkeyClient) {
1341
+ try {
1342
+ const session = await turnkey.getSession();
1343
+ if (!session?.organizationId) {
1344
+ // No session = wallet user
1345
+ console.log('[TurnkeyIntegration] Legacy: Wallet user detected (no session)');
1346
+ setOrganizationId(storedSubOrgId);
1347
+ setTurnkeyClient(null);
1348
+ setWalletAddress(storedWalletAddress);
1349
+ // Set auth_type for future
1350
+ localStorage.setItem('turnkey_auth_type', 'wallet');
1351
+ return;
1352
+ } else {
1353
+ // Has session = passkey user
1354
+ console.log('[TurnkeyIntegration] Legacy: Passkey user detected (has session)');
1355
+ setOrganizationId(session.organizationId);
1356
+ setTurnkeyClient(passkeyClient);
1357
+ setWalletAddress(storedWalletAddress);
1358
+ // Set auth_type for future
1359
+ localStorage.setItem('turnkey_auth_type', 'passkey');
1360
+ return;
1361
+ }
1362
+ } catch (err) {
1363
+ console.log('[TurnkeyIntegration] Legacy: Session check failed, assuming wallet user');
1364
+ setOrganizationId(storedSubOrgId);
1365
+ setTurnkeyClient(null);
1366
+ setWalletAddress(storedWalletAddress);
1367
+ localStorage.setItem('turnkey_auth_type', 'wallet');
1368
+ return;
1369
+ }
1370
+ } else {
1371
+ // Turnkey SDK not ready, assume wallet user
1372
+ console.log('[TurnkeyIntegration] Legacy: Turnkey not ready, assuming wallet user');
1373
+ setOrganizationId(storedSubOrgId);
1374
+ setTurnkeyClient(null);
1375
+ setWalletAddress(storedWalletAddress);
1376
+ localStorage.setItem('turnkey_auth_type', 'wallet');
1377
+ return;
1378
+ }
1379
+ }
1380
+
1381
+ // NO STORED CREDENTIALS - clear state
1382
+ console.log('[TurnkeyIntegration] No stored credentials found, clearing state');
1383
+ setTurnkeyClient(null);
1384
+ setOrganizationId('');
1385
+ setWalletAddress(null);
1386
+ };
1387
+ checkAuthAndSetup();
1388
+ }, [turnkey, passkeyClient]);
1389
+
1390
+ // Create wallet client with deferred account (no passkey prompt until signing)
1391
+ useEffect(() => {
1392
+ const createWalletClient = async () => {
1393
+ console.log('[TurnkeyIntegration] createWalletClient check:', {
1394
+ hasOrganizationId: !!organizationId,
1395
+ hasTurnkeyClient: !!turnkeyClient,
1396
+ hasWalletAddress: !!walletAddress,
1397
+ hasTurnkeyWalletClient: !!turnkeyWalletClient,
1398
+ });
1399
+
1400
+ // Clear wallet client if session is lost
1401
+ if (!organizationId || !walletAddress) {
1402
+ if (turnkeyWalletClient) {
1403
+ console.log('[TurnkeyIntegration] Clearing wallet client - session ended');
1404
+ setTurnkeyWalletClient(null);
1405
+ }
1406
+ return;
1407
+ }
1408
+
1409
+ // Don't recreate if already exists
1410
+ if (turnkeyWalletClient) {
1411
+ console.log('[TurnkeyIntegration] Wallet client already exists, skipping creation');
1412
+ return;
1413
+ }
1414
+
1415
+ try {
1416
+ const { toAccount } = await import('viem/accounts');
1417
+ const { createWalletClient: createViemWalletClient, http, custom } = await import('viem');
1418
+
1419
+ // Check auth_type to determine which wallet client to create
1420
+ const storedAuthType = localStorage.getItem('turnkey_auth_type');
1421
+
1422
+ // Check if wallet is ready to connect (flag set after Connect Wallet success)
1423
+ const walletReady = localStorage.getItem('turnkey_wallet_ready');
1424
+
1425
+ // Create wallet client for WALLET users only if they just connected (flag exists)
1426
+ if (!turnkeyClient && storedAuthType === 'wallet' && walletReady === 'true' && typeof window !== 'undefined' && window.ethereum) {
1427
+ console.log('[TurnkeyIntegration] Creating MetaMask wallet client (post-connection)');
1428
+
1429
+ const ethereum = window.ethereum!;
1430
+
1431
+ // Connect to MetaMask and verify it's the same account
1432
+ const accounts = await ethereum.request({ method: 'eth_requestAccounts' }) as string[];
1433
+ const connectedAddress = accounts[0];
1434
+ console.log('[TurnkeyIntegration] Connected MetaMask address:', connectedAddress);
1435
+
1436
+ // Create wallet client from MetaMask
1437
+ const metamaskAccount = toAccount({
1438
+ address: connectedAddress as `0x${string}`,
1439
+ async signMessage({ message }) {
1440
+ console.log('[MetaMask] Signing message');
1441
+ return await (ethereum.request as any)({
1442
+ method: 'personal_sign',
1443
+ params: [typeof message === 'string' ? message : message.raw, connectedAddress],
1444
+ }) as `0x${string}`;
1445
+ },
1446
+ async signTransaction(_transaction) {
1447
+ throw new Error('signTransaction not supported for wallet-based authentication with ERC-4337');
1448
+ },
1449
+ async signTypedData(typedData) {
1450
+ console.log('[MetaMask] Signing typed data');
1451
+ return await (ethereum.request as any)({
1452
+ method: 'eth_signTypedData_v4',
1453
+ params: [connectedAddress, JSON.stringify(typedData)],
1454
+ }) as `0x${string}`;
1455
+ },
1456
+ });
1457
+
1458
+ const walletClient = createViemWalletClient({
1459
+ account: metamaskAccount,
1460
+ chain,
1461
+ transport: custom(ethereum),
1462
+ });
1463
+
1464
+ console.log('[TurnkeyIntegration] MetaMask wallet client created successfully');
1465
+ setTurnkeyWalletClient(walletClient);
1466
+ // Clear flag after successful creation
1467
+ localStorage.removeItem('turnkey_wallet_ready');
1468
+ } else if (turnkeyClient && storedAuthType === 'passkey') {
1469
+ console.log('[TurnkeyIntegration] Creating Turnkey deferred wallet client for passkey user');
1470
+
1471
+ // Create a DEFERRED account - no passkey prompt until actually signing
1472
+ const deferredAccount = toAccount({
1473
+ address: walletAddress as `0x${string}`,
1474
+ async signMessage({ message }) {
1475
+ console.log('[DeferredAccount] Signing message - will prompt for passkey now');
1476
+ const { createAccount } = await import('@turnkey/viem');
1477
+ const account = await createAccount({
1478
+ client: turnkeyClient!,
1479
+ organizationId,
1480
+ signWith: walletAddress,
1481
+ ethereumAddress: walletAddress,
1482
+ });
1483
+ return account.signMessage({ message });
1484
+ },
1485
+ async signTransaction(_transaction) {
1486
+ throw new Error('signTransaction not supported for Turnkey with ERC-4337');
1487
+ },
1488
+ async signTypedData(typedData) {
1489
+ console.log('[DeferredAccount] Signing typed data - will prompt for passkey now');
1490
+ const { createAccount } = await import('@turnkey/viem');
1491
+ const account = await createAccount({
1492
+ client: turnkeyClient!,
1493
+ organizationId,
1494
+ signWith: walletAddress,
1495
+ ethereumAddress: walletAddress,
1496
+ });
1497
+ return account.signTypedData(typedData);
1498
+ },
1499
+ });
1500
+
1501
+ const walletClient = createViemWalletClient({
1502
+ account: deferredAccount,
1503
+ chain,
1504
+ transport: http(rpcUrl || chain.rpcUrls.default.http[0]),
1505
+ });
1506
+
1507
+ console.log('[TurnkeyIntegration] Turnkey deferred wallet client created successfully (no passkey prompt yet)');
1508
+ setTurnkeyWalletClient(walletClient);
1509
+ } else {
1510
+ // Waiting for Turnkey client to initialize for passkey users, or wrong configuration
1511
+ if (storedAuthType === 'passkey' && !turnkeyClient) {
1512
+ console.log('[TurnkeyIntegration] Waiting for Turnkey SDK to initialize for passkey user...');
1513
+ } else {
1514
+ console.log('[TurnkeyIntegration] Cannot create wallet client - unexpected state:', {
1515
+ storedAuthType,
1516
+ hasTurnkeyClient: !!turnkeyClient,
1517
+ hasMetaMask: typeof window !== 'undefined' && !!window.ethereum,
1518
+ });
1519
+ }
1520
+ }
1521
+ } catch (err) {
1522
+ console.error('[TurnkeyIntegration] Failed to create wallet client:', err);
1523
+ }
1524
+ };
1525
+ createWalletClient();
1526
+ }, [organizationId, turnkeyClient, walletAddress]);
1527
+
1528
+ // Only initialize SBC after wallet client is ready
1529
+ const sbcResult = useSbcTurnkey({
1530
+ apiKey: import.meta.env.VITE_SBC_API_KEY,
1531
+ chain,
1532
+ turnkeyClient: turnkeyWalletClient ? turnkeyClient : null, // Only pass if wallet client is ready
1533
+ organizationId: turnkeyWalletClient ? organizationId : '',
1534
+ rpcUrl,
1535
+ debug: true,
1536
+ turnkeyWalletClient, // Pass the pre-created wallet client
1537
+ });
1538
+
1539
+ // Debug: Show initialization status
1540
+ console.log('[TurnkeyIntegration] SBC Result:', {
1541
+ isInitialized: sbcResult.isInitialized,
1542
+ hasError: !!sbcResult.error,
1543
+ error: sbcResult.error?.message,
1544
+ hasTurnkeyClient: !!turnkeyClient,
1545
+ organizationId,
1546
+ });
1547
+
1548
+ return (
1549
+ <>
1550
+ <TurnkeyAuth
1551
+ ownerAddress={sbcResult.ownerAddress}
1552
+ account={sbcResult.account}
1553
+ turnkeyWalletClient={turnkeyWalletClient}
1554
+ />
1555
+
1556
+ {/* Show error if any */}
1557
+ {sbcResult.error && (
1558
+ <div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
1559
+ <p className="text-sm font-medium text-red-800">❌ SBC Initialization Error</p>
1560
+ <p className="text-xs text-red-600 mt-1">{sbcResult.error.message}</p>
1561
+ </div>
1562
+ )}
1563
+
1564
+ {/* Show smart account initialization state */}
1565
+ {!sbcResult.isInitialized && !sbcResult.error && turnkeyClient && (
1566
+ <div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
1567
+ <p className="text-sm text-blue-800 mb-2">⏳ Initializing smart account...</p>
1568
+ <p className="text-xs text-blue-600">
1569
+ Setting up your account abstraction wallet. You'll be prompted for your passkey when sending transactions.
1570
+ </p>
1571
+ </div>
1572
+ )}
1573
+
1574
+ {sbcResult.isInitialized && turnkeyWalletClient && (
1575
+ <>
1576
+ <SmartAccountInfo
1577
+ account={sbcResult.account}
1578
+ isInitialized={sbcResult.isInitialized}
1579
+ refreshAccount={sbcResult.refreshAccount}
1580
+ isLoadingAccount={sbcResult.isLoadingAccount}
1581
+ />
1582
+ <TransactionForm
1583
+ sbcAppKit={sbcResult.sbcAppKit}
1584
+ account={sbcResult.account}
1585
+ ownerAddress={sbcResult.ownerAddress}
1586
+ />
1587
+ </>
1588
+ )}
1589
+ </>
1590
+ );
1591
+ }
1592
+
1593
+ export default function App() {
1594
+ return (
1595
+ <TurnkeyProvider config={turnkeyConfig}>
1596
+ <div className="min-h-screen bg-gray-50 py-8">
1597
+ <div className="max-w-2xl mx-auto px-4">
1598
+ <div className="text-center mb-8">
1599
+ <h1 className="text-3xl font-bold text-gray-900 mb-2 flex items-center justify-center gap-3">
1600
+ <img src="/sbc-logo.png" alt="SBC Logo" width={36} height={36} />
1601
+ SBC + Turnkey Integration
1602
+ </h1>
1603
+ <p className="text-gray-600">Embedded wallet smart accounts with Turnkey passkey authentication</p>
1604
+ </div>
1605
+ <TurnkeyIntegration />
1606
+ <div className="mt-8 text-center text-xs text-gray-500">
1607
+ <p>
1608
+ Powered by{' '}
1609
+ <a href="https://stablecoin.xyz" className="text-blue-600 hover:underline">SBC App Kit</a>
1610
+ {' '}+{' '}
1611
+ <a href="https://turnkey.com" className="text-blue-600 hover:underline">Turnkey</a>
1612
+ </p>
1613
+ </div>
1614
+ </div>
1615
+ </div>
1616
+ </TurnkeyProvider>
1617
+ );
1618
+ }
1619
+
1620
+ async function getPermitSignature({
1621
+ publicClient,
1622
+ walletClient,
1623
+ owner,
1624
+ spender,
1625
+ value,
1626
+ tokenAddress,
1627
+ chainId,
1628
+ deadline,
1629
+ }: {
1630
+ publicClient: PublicClient;
1631
+ walletClient: WalletClient;
1632
+ owner: string;
1633
+ spender: string;
1634
+ value: bigint;
1635
+ tokenAddress: string;
1636
+ chainId: number;
1637
+ deadline: number;
1638
+ }): Promise<`0x${string}` | null> {
1639
+ try {
1640
+ const ownerChecksum = getAddress(owner);
1641
+ const spenderChecksum = getAddress(spender);
1642
+
1643
+ const nonce = await publicClient.readContract({
1644
+ address: tokenAddress as `0x${string}`,
1645
+ abi: erc20PermitAbi,
1646
+ functionName: 'nonces',
1647
+ args: [ownerChecksum],
1648
+ });
1649
+
1650
+ const tokenName = await publicClient.readContract({
1651
+ address: tokenAddress as `0x${string}`,
1652
+ abi: erc20PermitAbi,
1653
+ functionName: 'name',
1654
+ });
1655
+
1656
+ const domain = {
1657
+ name: tokenName as string,
1658
+ version: '1',
1659
+ chainId: BigInt(chainId),
1660
+ verifyingContract: SBC_TOKEN_ADDRESS(chain) as `0x${string}`,
1661
+ };
1662
+
1663
+ const types = {
1664
+ EIP712Domain: [
1665
+ { name: 'name', type: 'string' },
1666
+ { name: 'version', type: 'string' },
1667
+ { name: 'chainId', type: 'uint256' },
1668
+ { name: 'verifyingContract', type: 'address' },
1669
+ ],
1670
+ Permit: [
1671
+ { name: 'owner', type: 'address' },
1672
+ { name: 'spender', type: 'address' },
1673
+ { name: 'value', type: 'uint256' },
1674
+ { name: 'nonce', type: 'uint256' },
1675
+ { name: 'deadline', type: 'uint256' },
1676
+ ],
1677
+ } as const;
1678
+
1679
+ const message = {
1680
+ owner: ownerChecksum,
1681
+ spender: spenderChecksum,
1682
+ value: value,
1683
+ nonce: nonce as bigint,
1684
+ deadline: BigInt(deadline),
1685
+ };
1686
+
1687
+ // walletClient.account is available on the client
1688
+ const signature = await walletClient.signTypedData({
1689
+ account: walletClient.account!,
1690
+ domain,
1691
+ types,
1692
+ primaryType: 'Permit',
1693
+ message,
1694
+ });
1695
+
1696
+ return signature;
1697
+ } catch (e) {
1698
+ console.error('Error in getPermitSignature:', e);
1699
+ return null;
1700
+ }
1701
+ }