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.
- package/README.md +75 -5
- package/bin/cli.js +13 -5
- package/package.json +1 -1
- package/templates/README.md +53 -4
- package/templates/react/README.md.template +8 -9
- package/templates/react-dynamic/.env.template +1 -1
- package/templates/react-dynamic/README.md.template +131 -13
- package/templates/react-para/.env.template +1 -1
- package/templates/react-para/README.md.template +154 -13
- package/templates/react-turnkey/.env.template +26 -0
- package/templates/react-turnkey/README.md.template +206 -0
- package/templates/react-turnkey/eslint.config.js.template +32 -0
- package/templates/react-turnkey/index.html.template +14 -0
- package/templates/react-turnkey/package.json.template +45 -0
- package/templates/react-turnkey/public/sbc-logo.png +0 -0
- package/templates/react-turnkey/server/index.ts.template +271 -0
- package/templates/react-turnkey/src/App.tsx.template +1701 -0
- package/templates/react-turnkey/src/env.d.ts.template +17 -0
- package/templates/react-turnkey/src/index.css.template +16 -0
- package/templates/react-turnkey/src/main.tsx.template +11 -0
- package/templates/react-turnkey/tsconfig.json.template +25 -0
- package/templates/react-turnkey/tsconfig.node.json.template +10 -0
- package/templates/react-turnkey/vite.config.ts.template +10 -0
|
@@ -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
|
+
}
|