@jaw.id/ui 0.0.3 → 0.0.5

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.
Files changed (66) hide show
  1. package/cjs-error.cjs +3 -0
  2. package/dist/{ccip-BYa9WTP9.js → ccip-nTJDT8tV.js} +1 -1
  3. package/dist/{index-DBYNWeek.js → index-C2yN5Jh0.js} +20536 -20246
  4. package/dist/index.js +1 -1
  5. package/package.json +8 -2
  6. package/.babelrc +0 -12
  7. package/CHANGELOG.md +0 -23
  8. package/components.json +0 -22
  9. package/postcss.config.cjs +0 -6
  10. package/src/components/ConnectDialog/index.tsx +0 -270
  11. package/src/components/ConnectDialog/types.ts +0 -34
  12. package/src/components/DefaultDialog/index.tsx +0 -99
  13. package/src/components/Eip712Dialog/index.tsx +0 -525
  14. package/src/components/Eip712Dialog/types.ts +0 -27
  15. package/src/components/FeeTokenSelector/index.tsx +0 -308
  16. package/src/components/OnboardingDialog/index.tsx +0 -317
  17. package/src/components/OnboardingDialog/types.ts +0 -43
  18. package/src/components/OrSeparator/index.tsx +0 -13
  19. package/src/components/PermissionDialog/index.tsx +0 -598
  20. package/src/components/PermissionDialog/types.ts +0 -77
  21. package/src/components/SignatureDialog/index.tsx +0 -180
  22. package/src/components/SignatureDialog/types.ts +0 -27
  23. package/src/components/SiweDialog/index.tsx +0 -231
  24. package/src/components/SiweDialog/types.ts +0 -34
  25. package/src/components/TransactionDialog/DecodedCalldata.tsx +0 -79
  26. package/src/components/TransactionDialog/index.tsx +0 -663
  27. package/src/components/TransactionDialog/types.ts +0 -54
  28. package/src/components/ui/accordion.tsx +0 -66
  29. package/src/components/ui/avatar.tsx +0 -53
  30. package/src/components/ui/button.tsx +0 -59
  31. package/src/components/ui/card.tsx +0 -92
  32. package/src/components/ui/checkbox.tsx +0 -32
  33. package/src/components/ui/dialog.tsx +0 -183
  34. package/src/components/ui/dropdown-menu.tsx +0 -258
  35. package/src/components/ui/form.tsx +0 -167
  36. package/src/components/ui/input.tsx +0 -60
  37. package/src/components/ui/label.tsx +0 -24
  38. package/src/components/ui/popover.tsx +0 -48
  39. package/src/components/ui/scroll-area.tsx +0 -58
  40. package/src/components/ui/select.tsx +0 -160
  41. package/src/components/ui/separator.tsx +0 -28
  42. package/src/components/ui/sheet.tsx +0 -169
  43. package/src/components/ui/spinner.tsx +0 -18
  44. package/src/components/ui/tabs.tsx +0 -69
  45. package/src/components/ui/tooltip.tsx +0 -61
  46. package/src/hooks/index.ts +0 -5
  47. package/src/hooks/useChainIconURI.tsx +0 -117
  48. package/src/hooks/useDecodedCalldata.ts +0 -128
  49. package/src/hooks/useFeeTokenPrice.tsx +0 -36
  50. package/src/hooks/useGasEstimation.ts +0 -474
  51. package/src/hooks/useIsMobile.ts +0 -36
  52. package/src/icons/index.tsx +0 -114
  53. package/src/index.ts +0 -19
  54. package/src/lib/utils.ts +0 -6
  55. package/src/react/ReactUIHandler.tsx +0 -3004
  56. package/src/react/index.ts +0 -2
  57. package/src/styles.css +0 -210
  58. package/src/utils/formatAddress.ts +0 -24
  59. package/src/utils/index.ts +0 -4
  60. package/src/utils/justaNameInstance.ts +0 -25
  61. package/src/utils/tokenBalance.ts +0 -41
  62. package/src/utils/tokenPrice.ts +0 -58
  63. package/tailwind.config.js +0 -130
  64. package/tsconfig.json +0 -19
  65. package/tsconfig.lib.json +0 -43
  66. package/vite.config.ts +0 -45
@@ -1,3004 +0,0 @@
1
- 'use client';
2
-
3
- import React, { useMemo, useState, useEffect } from 'react';
4
- import { createRoot } from 'react-dom/client';
5
- import {
6
- UIHandler,
7
- UIHandlerConfig,
8
- UIRequest,
9
- UIResponse,
10
- UIError,
11
- UIErrorCode,
12
- ConnectUIRequest,
13
- SignatureUIRequest,
14
- TypedDataUIRequest,
15
- TransactionUIRequest,
16
- SendTransactionUIRequest,
17
- PermissionUIRequest,
18
- RevokePermissionUIRequest,
19
- WalletSignUIRequest,
20
- Account,
21
- SUPPORTED_CHAINS,
22
- JAW_RPC_URL,
23
- JAW_PAYMASTER_URL,
24
- SubnameTextRecordCapabilityRequest,
25
- getPermissionFromRelay,
26
- handleGetCapabilitiesRequest,
27
- buildGrantPermissionCall,
28
- buildRevokePermissionCall,
29
- type Chain,
30
- type SignInWithEthereumCapabilityRequest,
31
- type PaymasterConfig,
32
- type FeeTokenCapability,
33
- ensureIntNumber,
34
- standardErrorCodes,
35
- } from '@jaw.id/core';
36
- import { formatUnits, erc20Abi, createPublicClient, http } from 'viem';
37
- import type { Address, Hex } from 'viem';
38
- import { createSiweMessage } from 'viem/siwe';
39
-
40
- // Import UI components using relative paths (we're inside @jaw.id/ui)
41
- import { OnboardingDialog } from '../components/OnboardingDialog';
42
- import { DefaultDialog, type DefaultDialogProps } from '../components/DefaultDialog';
43
- import { SignatureDialog } from '../components/SignatureDialog';
44
- import { SiweDialog } from '../components/SiweDialog';
45
- import { Eip712Dialog } from '../components/Eip712Dialog';
46
- import { TransactionDialog } from '../components/TransactionDialog';
47
- import { PermissionDialog } from '../components/PermissionDialog';
48
- import { ConnectDialog } from '../components/ConnectDialog';
49
- import { type FeeTokenOption } from '../components/FeeTokenSelector';
50
- import { type LocalStorageAccount, type CreatedAccountData } from '../components/OnboardingDialog/types';
51
- import { useChainIconURI } from '../hooks/useChainIconURI';
52
- import { useFeeTokenPrice } from '../hooks/useFeeTokenPrice';
53
- import { useGasEstimation } from '../hooks/useGasEstimation';
54
- import { fetchTokenBalance, isNativeToken } from '../utils/tokenBalance';
55
-
56
-
57
- /**
58
- * Converts hex string to UTF-8 string
59
- */
60
- function hexToUtf8(hex: string): string {
61
- const hexString = hex.startsWith('0x') ? hex.slice(2) : hex;
62
- const bytes = new Uint8Array(hexString.length / 2);
63
- for (let i = 0; i < hexString.length; i += 2) {
64
- bytes[i / 2] = parseInt(hexString.slice(i, i + 2), 16);
65
- }
66
- return new TextDecoder().decode(bytes);
67
- }
68
-
69
- /**
70
- * Detects if a message is a SIWE (Sign-In with Ethereum) message
71
- * according to EIP-4361 specification
72
- */
73
- function isSiweMessage(message: string): boolean {
74
- if (!message) return false;
75
-
76
- try {
77
- // If message is hex-encoded, decode it first
78
- let decodedMessage = message;
79
- if (message.startsWith('0x')) {
80
- decodedMessage = hexToUtf8(message);
81
- }
82
-
83
- // Primary detection: Check for the SIWE signature phrase
84
- const hasSiwePhrase = decodedMessage.includes('wants you to sign in with your Ethereum account');
85
-
86
- if (!hasSiwePhrase) {
87
- return false;
88
- }
89
-
90
- // Additional validation: Check for required SIWE fields
91
- const hasUri = /URI:\s*.+/.test(decodedMessage);
92
- const hasVersion = /Version:\s*1/.test(decodedMessage);
93
- const hasChainId = /Chain ID:\s*\d+/.test(decodedMessage);
94
- const hasNonce = /Nonce:\s*[a-zA-Z0-9]{8,}/.test(decodedMessage);
95
- const hasIssuedAt = /Issued At:\s*.+/.test(decodedMessage);
96
-
97
- return hasSiwePhrase && hasUri && hasVersion && hasChainId && hasNonce && hasIssuedAt;
98
- } catch (error) {
99
- console.error('Error checking if message is SIWE:', error);
100
- return false;
101
- }
102
- }
103
-
104
- // ============================================================================
105
- // Chain Utilities
106
- // ============================================================================
107
-
108
- /**
109
- * Get chain name from chain ID
110
- */
111
- function getChainNameFromId(chainId: number): string {
112
- const chain = SUPPORTED_CHAINS.find(c => c.id === chainId);
113
- return chain?.name || 'Unknown Network';
114
- }
115
-
116
- // ============================================================================
117
- // Permission Utilities
118
- // ============================================================================
119
-
120
- // Format timestamp to readable date
121
- const formatExpiryDate = (timestamp: number): string => {
122
- const date = new Date(timestamp * 1000);
123
- return date.toLocaleString('en-US', {
124
- day: '2-digit',
125
- month: '2-digit',
126
- year: 'numeric',
127
- hour: '2-digit',
128
- minute: '2-digit',
129
- second: '2-digit',
130
- hour12: false,
131
- });
132
- };
133
-
134
- // Token info cache type
135
- type TokenInfoMap = Record<string, { decimals: number; symbol: string }>;
136
-
137
- // Type assertion to fix React types version mismatch
138
- const DefaultDialogComponent: React.ComponentType<DefaultDialogProps> = DefaultDialog as React.ComponentType<DefaultDialogProps>;
139
-
140
- /**
141
- * React UI handler for app-specific mode
142
- *
143
- * This handler is automatically initialized by the SDK with the necessary configuration.
144
- * Simply pass a new instance to JAW.create() and the SDK will call init() with the config.
145
- *
146
- * @example
147
- * ```typescript
148
- * import { JAW, Mode } from '@jaw.id/core';
149
- * import { ReactUIHandler } from '@jaw.id/ui';
150
- *
151
- * const jaw = JAW.create({
152
- * apiKey: 'your-api-key',
153
- * defaultChainId: 1,
154
- * preference: {
155
- * mode: Mode.AppSpecific,
156
- * uiHandler: new ReactUIHandler(),
157
- * },
158
- * });
159
- * ```
160
- */
161
- export class ReactUIHandler implements UIHandler {
162
- private config: UIHandlerConfig = {} as UIHandlerConfig;
163
-
164
- /**
165
- * Initialize the handler with SDK configuration
166
- * Called automatically by the SDK - do not call directly
167
- */
168
- init(config: UIHandlerConfig): void {
169
- this.config = config;
170
- }
171
-
172
- async request<T = unknown>(request: UIRequest): Promise<UIResponse<T>> {
173
- return new Promise((resolve, reject) => {
174
- try {
175
- const container = document.createElement('div');
176
- container.setAttribute('data-jaw-modal-container', '');
177
-
178
- // Append to body - Radix UI Dialog will handle all positioning
179
- document.body.appendChild(container);
180
-
181
- const root = createRoot(container);
182
-
183
- const cleanup = () => {
184
- try {
185
- document.body.style.removeProperty('pointer-events');
186
-
187
- root.unmount();
188
- if (container.parentNode) {
189
- container.parentNode.removeChild(container);
190
- }
191
- } catch (cleanupError) {
192
- console.error('[ReactUIHandler] Cleanup error:', cleanupError);
193
- }
194
- };
195
-
196
- const handleApprove = (data: T) => {
197
- cleanup();
198
- resolve({
199
- id: request.id,
200
- approved: true,
201
- data,
202
- });
203
- };
204
-
205
- const handleReject = (error?: Error) => {
206
- cleanup();
207
- const response = {
208
- id: request.id,
209
- approved: false,
210
- error: error as UIError || UIError.userRejected(),
211
- };
212
- resolve(response);
213
- };
214
-
215
- // Render appropriate dialog based on request type
216
- console.log('[ReactUIHandler] Rendering dialog for request type:', request.type);
217
- const dialog = this.renderDialog(request, handleApprove, handleReject);
218
- root.render(dialog);
219
- console.log('[ReactUIHandler] Dialog rendered');
220
- } catch (error) {
221
- console.error('[ReactUIHandler] Error in request:', error);
222
- reject(error);
223
- }
224
- });
225
- }
226
-
227
- canHandle(request: UIRequest): boolean {
228
- return [
229
- 'wallet_connect',
230
- 'personal_sign',
231
- 'eth_signTypedData_v4',
232
- 'wallet_sendCalls',
233
- 'eth_sendTransaction',
234
- 'wallet_grantPermissions',
235
- 'wallet_revokePermissions',
236
- 'wallet_sign',
237
- ].includes(request.type);
238
- }
239
-
240
- async cleanup(): Promise<void> {
241
- // Cleanup any remaining modals
242
- const containers = document.querySelectorAll('[data-jaw-modal-container]');
243
- containers.forEach((container: Element) => {
244
- if (container.parentNode) {
245
- container.parentNode.removeChild(container);
246
- }
247
- });
248
- }
249
-
250
- private renderDialog(
251
- request: UIRequest,
252
- onApprove: (data: any) => void,
253
- onReject: (error?: Error) => void
254
- ): React.ReactElement {
255
- switch (request.type) {
256
- case 'wallet_connect':
257
- return (
258
- <OnboardingDialogWrapper
259
- request={request as ConnectUIRequest}
260
- onApprove={onApprove}
261
- onReject={onReject}
262
- apiKey={this.config.apiKey}
263
- defaultChainId={this.config.defaultChainId}
264
- paymasters={this.config.paymasters}
265
- ens={this.config.ens}
266
- />
267
- );
268
-
269
- case 'personal_sign': {
270
- const signRequest = request as SignatureUIRequest;
271
- // Check if this is a SIWE message
272
- if (isSiweMessage(signRequest.data.message)) {
273
- return (
274
- <SiweDialogWrapper
275
- request={signRequest}
276
- onApprove={onApprove}
277
- onReject={onReject}
278
- apiKey={this.config.apiKey}
279
- defaultChainId={this.config.defaultChainId}
280
- paymasters={this.config.paymasters}
281
- />
282
- );
283
- }
284
- return (
285
- <SignatureDialogWrapper
286
- request={signRequest}
287
- onApprove={onApprove}
288
- onReject={onReject}
289
- apiKey={this.config.apiKey}
290
- defaultChainId={this.config.defaultChainId}
291
- paymasters={this.config.paymasters}
292
- />
293
- );
294
- }
295
-
296
- case 'wallet_sign': {
297
- const walletSignRequest = request as WalletSignUIRequest;
298
- const signType = walletSignRequest.data.request.type;
299
-
300
- if (signType === '0x45') {
301
- // ERC-7871 PersonalSign - data is { message: string }
302
- const requestData = walletSignRequest.data.request.data as { message: string };
303
- const message = requestData.message;
304
- if (isSiweMessage(message)) {
305
- return (
306
- <SiweDialogWrapper
307
- request={{
308
- ...walletSignRequest,
309
- type: 'personal_sign',
310
- data: {
311
- message,
312
- address: walletSignRequest.data.address,
313
- chainId: walletSignRequest.data.chainId,
314
- },
315
- } as SignatureUIRequest}
316
- onApprove={onApprove}
317
- onReject={onReject}
318
- apiKey={this.config.apiKey}
319
- defaultChainId={this.config.defaultChainId}
320
- paymasters={this.config.paymasters}
321
- />
322
- );
323
- }
324
- return (
325
- <SignatureDialogWrapper
326
- request={{
327
- ...walletSignRequest,
328
- type: 'personal_sign',
329
- data: {
330
- message,
331
- address: walletSignRequest.data.address,
332
- chainId: walletSignRequest.data.chainId,
333
- },
334
- } as SignatureUIRequest}
335
- onApprove={onApprove}
336
- onReject={onReject}
337
- apiKey={this.config.apiKey}
338
- defaultChainId={this.config.defaultChainId}
339
- paymasters={this.config.paymasters}
340
- />
341
- );
342
- } else if (signType === '0x01') {
343
- // ERC-7871 TypedData - data can be either a JSON string or an object
344
- const typedDataRaw = walletSignRequest.data.request.data;
345
- // If it's already a string, use it directly; otherwise JSON.stringify it
346
- const typedDataJson = typeof typedDataRaw === 'string'
347
- ? typedDataRaw
348
- : JSON.stringify(typedDataRaw);
349
- return (
350
- <Eip712DialogWrapper
351
- request={{
352
- ...walletSignRequest,
353
- type: 'eth_signTypedData_v4',
354
- data: {
355
- typedData: typedDataJson,
356
- address: walletSignRequest.data.address,
357
- chainId: walletSignRequest.data.chainId,
358
- },
359
- } as TypedDataUIRequest}
360
- onApprove={onApprove}
361
- onReject={onReject}
362
- apiKey={this.config.apiKey}
363
- defaultChainId={this.config.defaultChainId}
364
- paymasters={this.config.paymasters}
365
- />
366
- );
367
- } else {
368
- // Unsupported sign type
369
- return (
370
- <UnsupportedMethodDialogWrapper
371
- method={`wallet_sign (type: ${signType})`}
372
- onReject={onReject}
373
- />
374
- );
375
- }
376
- }
377
-
378
- case 'eth_signTypedData_v4':
379
- return (
380
- <Eip712DialogWrapper
381
- request={request as TypedDataUIRequest}
382
- onApprove={onApprove}
383
- onReject={onReject}
384
- apiKey={this.config.apiKey}
385
- defaultChainId={this.config.defaultChainId}
386
- paymasters={this.config.paymasters}
387
- />
388
- );
389
-
390
- case 'wallet_sendCalls':
391
- return (
392
- <TransactionDialogWrapper
393
- request={request as TransactionUIRequest}
394
- onApprove={onApprove}
395
- onReject={onReject}
396
- apiKey={this.config.apiKey}
397
- defaultChainId={this.config.defaultChainId}
398
- paymasters={this.config.paymasters}
399
- />
400
- );
401
-
402
- case 'eth_sendTransaction':
403
- return (
404
- <SendTransactionDialogWrapper
405
- request={request as SendTransactionUIRequest}
406
- onApprove={onApprove}
407
- onReject={onReject}
408
- apiKey={this.config.apiKey}
409
- defaultChainId={this.config.defaultChainId}
410
- paymasters={this.config.paymasters}
411
- />
412
- );
413
-
414
- case 'wallet_grantPermissions':
415
- return (
416
- <PermissionDialogWrapper
417
- request={request as PermissionUIRequest}
418
- onApprove={onApprove}
419
- onReject={onReject}
420
- apiKey={this.config.apiKey}
421
- defaultChainId={this.config.defaultChainId}
422
- paymasters={this.config.paymasters}
423
- />
424
- );
425
-
426
- case 'wallet_revokePermissions':
427
- return (
428
- <RevokePermissionDialogWrapper
429
- request={request as RevokePermissionUIRequest}
430
- onApprove={onApprove}
431
- onReject={onReject}
432
- apiKey={this.config.apiKey}
433
- defaultChainId={this.config.defaultChainId}
434
- paymasters={this.config.paymasters}
435
- />
436
- );
437
-
438
- default: {
439
- // Fallback for unsupported methods
440
- return (
441
- <UnsupportedMethodDialogWrapper
442
- method={(request as UIRequest).type}
443
- onReject={onReject}
444
- />
445
- );
446
- }
447
- }
448
- }
449
- }
450
-
451
- // Helper to build chain config with auto-resolved RPC URL from API key
452
- // Used by OnboardingDialogWrapper which needs lower-level control
453
- function buildChainConfigFromApiKey(chainId: number, apiKey?: string, paymasterUrl?: string): Chain {
454
- return {
455
- id: chainId,
456
- rpcUrl: apiKey ? `${JAW_RPC_URL}?chainId=${chainId}&api-key=${apiKey}` : `${JAW_RPC_URL}?chainId=${chainId}`,
457
- ...(paymasterUrl && { paymaster: { url: paymasterUrl } }),
458
- };
459
- }
460
-
461
- // Helper to build mainnet RPC URL for JustaName SDK (ENS resolution always uses mainnet)
462
- function getMainnetRpcUrl(apiKey?: string): string {
463
- return apiKey ? `${JAW_RPC_URL}?chainId=1&api-key=${apiKey}` : `${JAW_RPC_URL}?chainId=1`;
464
- }
465
-
466
- // Helper to get Account for signing operations
467
- async function getAccountForSigning(
468
- apiKey?: string,
469
- chainId?: number,
470
- paymasterUrl?: string
471
- ): Promise<Account> {
472
- if (!apiKey) {
473
- throw new Error('API key is required for signing operations');
474
- }
475
- const targetChainId = chainId || 1;
476
- return await Account.get({
477
- chainId: targetChainId,
478
- apiKey,
479
- paymasterUrl,
480
- });
481
- }
482
-
483
- // OnboardingDialogWrapper - handles passkey authentication flow with ConnectDialog confirmation
484
- function OnboardingDialogWrapper({
485
- request,
486
- onApprove,
487
- onReject,
488
- apiKey,
489
- defaultChainId,
490
- paymasters,
491
- ens,
492
- }: {
493
- request: ConnectUIRequest;
494
- onApprove: (data: any) => void;
495
- onReject: (error?: Error) => void;
496
- apiKey?: string;
497
- defaultChainId?: number;
498
- paymasters?: Record<number, PaymasterConfig>;
499
- ens?: string;
500
- }) {
501
- const [open, setOpen] = useState(true);
502
- const [accounts, setAccounts] = useState<LocalStorageAccount[]>([]);
503
- const [loggingInAccount, setLoggingInAccount] = useState<string | null>(null);
504
- const [isImporting, setIsImporting] = useState(false);
505
- const [isCreating, setIsCreating] = useState(false);
506
-
507
- // State for ConnectDialog confirmation
508
- const [showConnectDialog, setShowConnectDialog] = useState(false);
509
- const [authenticatedAccountName, setAuthenticatedAccountName] = useState<string | null>(null);
510
- const [authenticatedWalletAddress, setAuthenticatedWalletAddress] = useState<string | null>(null);
511
- const [isConnecting, setIsConnecting] = useState(false);
512
-
513
- // State for SIWE signing flow
514
- const [isSiweSigning, setIsSiweSigning] = useState(false);
515
- const [siweStatus, setSiweStatus] = useState<string>('');
516
-
517
- // Get rpId from current domain
518
- const rpId = typeof window !== 'undefined' ? window.location.hostname : 'localhost';
519
- const rpName = 'JAW';
520
- const origin = typeof window !== 'undefined' ? window.location.origin : 'unknown';
521
-
522
- // Get chain info for ConnectDialog
523
- const targetChainId = request.data.chainId || defaultChainId || 1;
524
- const chainName = getChainNameFromId(targetChainId);
525
- const chainIcon = useChainIconURI(targetChainId, apiKey, 24);
526
-
527
- // Load accounts on mount using Account class
528
- useEffect(() => {
529
- const loadAccounts = () => {
530
- const storedAccounts = Account.getStoredAccounts(apiKey);
531
- setAccounts(storedAccounts.map(acc => ({
532
- username: acc.username,
533
- creationDate: new Date(acc.creationDate),
534
- credentialId: acc.credentialId,
535
- isImported: acc.isImported,
536
- })));
537
- };
538
- loadAccounts();
539
- }, [apiKey]);
540
-
541
- // Silent mode: check for existing auth state and use it directly
542
- // This mirrors the cross-platform behavior where jaw:passkey:authState is checked
543
- useEffect(() => {
544
- if (!request.data.silent) {
545
- return;
546
- }
547
-
548
- // Check if there's an existing authenticated session
549
- const authenticatedAddress = Account.getAuthenticatedAddress(apiKey);
550
-
551
- if (authenticatedAddress) {
552
- // User is already authenticated - approve immediately without showing any UI
553
- // Use setTimeout to defer the call and avoid unmounting during render
554
- console.log('🔇 Silent mode: using existing auth state, address:', authenticatedAddress);
555
- setTimeout(() => {
556
- onApprove({
557
- accounts: [{ address: authenticatedAddress }],
558
- });
559
- }, 0);
560
- }
561
- // If not authenticated, fall back to showing OnboardingDialog
562
- // (the normal UI flow will handle account creation/login)
563
- }, [request.data.silent, apiKey, onApprove]);
564
-
565
- const handleCancel = () => {
566
- setOpen(false);
567
- setShowConnectDialog(false);
568
- onReject(UIError.userRejected());
569
- };
570
-
571
- // Handle ConnectDialog confirmation
572
- const handleConnectConfirm = () => {
573
- if (authenticatedWalletAddress) {
574
- setIsConnecting(true);
575
- console.log('🔗 User approved connection');
576
- onApprove({
577
- accounts: [{ address: authenticatedWalletAddress }],
578
- });
579
- }
580
- };
581
-
582
- // Handle ConnectDialog cancel
583
- const handleConnectCancel = () => {
584
- setShowConnectDialog(false);
585
- setAuthenticatedAccountName(null);
586
- setAuthenticatedWalletAddress(null);
587
- setLoggingInAccount(null);
588
- // Return to account selection
589
- };
590
-
591
- // Handle selecting an existing account
592
- const handleAccountSelect = async (account: LocalStorageAccount) => {
593
- if (!account.credentialId) {
594
- console.error('No credential ID for account');
595
- return;
596
- }
597
-
598
- try {
599
- if (!apiKey) {
600
- throw new Error('API key is required');
601
- }
602
- setLoggingInAccount(account.username);
603
-
604
- // Use Account.get which handles WebAuthn authentication and stores auth state
605
- const accountInstance = await Account.get(
606
- {
607
- chainId: targetChainId,
608
- apiKey,
609
- paymasterUrl: paymasters?.[targetChainId]?.url,
610
- },
611
- account.credentialId
612
- );
613
-
614
- // If silent mode, skip ConnectDialog and approve immediately
615
- if (request.data.silent) {
616
- console.log('🔇 Silent mode: skipping connect confirmation');
617
- onApprove({
618
- accounts: [{ address: accountInstance.address }],
619
- });
620
- return;
621
- }
622
-
623
- // If SIWE is requested, show ConnectDialog for confirmation
624
- if (signInWithEthereumCapability) {
625
- setAuthenticatedAccountName(account.username);
626
- setAuthenticatedWalletAddress(accountInstance.address);
627
- setShowConnectDialog(true);
628
- return;
629
- }
630
-
631
- // No SIWE: skip ConnectDialog and approve immediately in app-specific mode
632
- onApprove({
633
- accounts: [{ address: accountInstance.address }],
634
- });
635
- } catch (error) {
636
- console.error('Login failed:', error);
637
- setLoggingInAccount(null);
638
- }
639
- };
640
-
641
- // Handle importing an existing passkey from cloud
642
- const handleImportAccount = async () => {
643
- try {
644
- if (!apiKey) {
645
- throw new Error('API key is required');
646
- }
647
- setIsImporting(true);
648
-
649
- // Use Account.import which handles everything
650
- const accountInstance = await Account.import({
651
- chainId: targetChainId,
652
- apiKey,
653
- paymasterUrl: paymasters?.[targetChainId]?.url,
654
- });
655
-
656
- const metadata = accountInstance.getMetadata();
657
-
658
- // If silent mode, skip ConnectDialog and approve immediately
659
- if (request.data.silent) {
660
- console.log('🔇 Silent mode: skipping connect confirmation');
661
- setIsImporting(false);
662
- onApprove({
663
- accounts: [{ address: accountInstance.address }],
664
- });
665
- return;
666
- }
667
-
668
- // If SIWE is requested, show ConnectDialog for confirmation
669
- if (signInWithEthereumCapability) {
670
- setAuthenticatedAccountName(metadata?.username || 'Imported Account');
671
- setAuthenticatedWalletAddress(accountInstance.address);
672
- setShowConnectDialog(true);
673
- setIsImporting(false);
674
- return;
675
- }
676
-
677
- // No SIWE: skip ConnectDialog and approve immediately in app-specific mode
678
- setIsImporting(false);
679
- onApprove({
680
- accounts: [{ address: accountInstance.address }],
681
- });
682
- } catch (error) {
683
- console.error('Import failed:', error);
684
- setIsImporting(false);
685
- }
686
- };
687
-
688
- // Handle creating a new account
689
- const handleCreateAccount = async (username: string): Promise<CreatedAccountData> => {
690
- try {
691
- if (!apiKey) {
692
- throw new Error('API key is required');
693
- }
694
- setIsCreating(true);
695
-
696
- // Get chainId from request or default
697
- const createChainId = request.data.chainId || defaultChainId || 1;
698
-
699
- // Construct full subname when ENS is enabled (e.g., "john.example.eth")
700
- const fullUsername = ensDomain ? `${username.trim()}.${ensDomain}` : username.trim();
701
-
702
- // Use Account.create which handles everything
703
- const accountInstance = await Account.create(
704
- {
705
- chainId: createChainId,
706
- apiKey,
707
- paymasterUrl: paymasters?.[createChainId]?.url,
708
- },
709
- {
710
- username: fullUsername,
711
- rpId,
712
- rpName,
713
- }
714
- );
715
-
716
- // Get full account data including credentialId and publicKey from stored accounts
717
- const storedAccounts = Account.getStoredAccounts(apiKey);
718
- const createdAccount = storedAccounts.find(acc => acc.username === fullUsername);
719
-
720
- if (!createdAccount) {
721
- throw new Error('Failed to retrieve created account data');
722
- }
723
-
724
- // Return full account data - OnboardingDialog will pass it to onAccountCreationComplete
725
- return {
726
- address: accountInstance.address,
727
- credentialId: createdAccount.credentialId,
728
- username: fullUsername,
729
- publicKey: createdAccount.publicKey,
730
- };
731
- } catch (error) {
732
- console.error('Account creation failed:', error);
733
- setIsCreating(false);
734
- throw error;
735
- }
736
- };
737
-
738
- // Handle account creation completion (after subname registration if applicable)
739
- // Account data flows through from onCreateAccount - no intermediate state needed
740
- const handleAccountCreationComplete = async (accountData: CreatedAccountData) => {
741
- // If silent mode, skip ConnectDialog and approve immediately
742
- if (request.data.silent) {
743
- console.log('🔇 Silent mode: skipping connect confirmation');
744
- setIsCreating(false);
745
- onApprove({
746
- accounts: [{ address: accountData.address }],
747
- });
748
- return;
749
- }
750
-
751
- // If SIWE is requested, show ConnectDialog for confirmation
752
- if (signInWithEthereumCapability) {
753
- setAuthenticatedAccountName(accountData.username || 'New Account');
754
- setAuthenticatedWalletAddress(accountData.address);
755
- setShowConnectDialog(true);
756
- setIsCreating(false);
757
- return;
758
- }
759
-
760
- // No SIWE: skip ConnectDialog and approve immediately in app-specific mode
761
- setIsCreating(false);
762
- onApprove({
763
- accounts: [{ address: accountData.address }],
764
- });
765
- };
766
-
767
- // Get config from request - capabilities is Record<string, unknown>
768
- const ensDomain = ens as string | undefined;
769
- console.log('🔗 ENS domain:', ensDomain);
770
- const chainId = request.data.chainId || defaultChainId || 1;
771
- const subnameTextRecords = request.data.capabilities?.subnameTextRecords as SubnameTextRecordCapabilityRequest | undefined;
772
-
773
- // Extract signInWithEthereum capability if present
774
- const signInWithEthereumCapability = request.data.capabilities?.signInWithEthereum as SignInWithEthereumCapabilityRequest | undefined;
775
-
776
- // Build SIWE message from capability if present
777
- const siweMessage = useMemo(() => {
778
- if (!signInWithEthereumCapability || !authenticatedWalletAddress) return null;
779
-
780
- try {
781
- // Extract domain and URI from origin
782
- let defaultDomain: string;
783
- let defaultUri: string;
784
-
785
- try {
786
- const url = new URL(origin);
787
- defaultDomain = url.host;
788
- defaultUri = origin;
789
- } catch {
790
- defaultDomain = origin;
791
- defaultUri = origin;
792
- }
793
-
794
- // Convert hex chainId to number
795
- const chainIdNumber = ensureIntNumber(signInWithEthereumCapability.chainId);
796
-
797
- return createSiweMessage({
798
- address: authenticatedWalletAddress as `0x${string}`,
799
- chainId: chainIdNumber,
800
- domain: signInWithEthereumCapability.domain || defaultDomain,
801
- nonce: signInWithEthereumCapability.nonce,
802
- uri: signInWithEthereumCapability.uri || defaultUri,
803
- version: '1',
804
- statement: signInWithEthereumCapability.statement,
805
- issuedAt: signInWithEthereumCapability.issuedAt ? new Date(signInWithEthereumCapability.issuedAt) : new Date(),
806
- expirationTime: signInWithEthereumCapability.expirationTime ? new Date(signInWithEthereumCapability.expirationTime) : undefined,
807
- notBefore: signInWithEthereumCapability.notBefore ? new Date(signInWithEthereumCapability.notBefore) : undefined,
808
- requestId: signInWithEthereumCapability.requestId,
809
- resources: signInWithEthereumCapability.resources,
810
- });
811
- } catch (error) {
812
- console.error('Failed to build SIWE message:', error);
813
- return null;
814
- }
815
- }, [signInWithEthereumCapability, authenticatedWalletAddress, origin]);
816
-
817
- // Handle SIWE sign action
818
- const handleSiweSign = async () => {
819
- if (!authenticatedWalletAddress || !siweMessage) return;
820
-
821
- setIsSiweSigning(true);
822
- setSiweStatus('Signing message...');
823
-
824
- try {
825
- // Restore account for signing
826
- const account = await getAccountForSigning(
827
- apiKey,
828
- targetChainId,
829
- paymasters?.[targetChainId]?.url
830
- );
831
-
832
- // Sign the SIWE message
833
- const signature = await account.signMessage(siweMessage);
834
-
835
- setSiweStatus('Sign-in successful!');
836
- console.log('🔗 User signed SIWE message');
837
-
838
- // Build response per ERC-7846 format with SIWE capability
839
- onApprove({
840
- accounts: [{
841
- address: authenticatedWalletAddress,
842
- capabilities: {
843
- signInWithEthereum: {
844
- message: siweMessage,
845
- signature: signature as `0x${string}`
846
- }
847
- }
848
- }]
849
- });
850
- } catch (error) {
851
- console.error('SIWE signature failed:', error);
852
- setSiweStatus(`Error: ${(error as Error).message}`);
853
- setIsSiweSigning(false);
854
- }
855
- };
856
-
857
- // Handle SIWE cancel
858
- const handleSiweCancel = () => {
859
- if (!isSiweSigning) {
860
- setShowConnectDialog(false);
861
- setAuthenticatedAccountName(null);
862
- setAuthenticatedWalletAddress(null);
863
- setLoggingInAccount(null);
864
- setSiweStatus('');
865
- // Return to account selection
866
- }
867
- };
868
-
869
- // If showing confirmation dialog and SIWE capability is present, show SiweDialog
870
- // Note: We check signInWithEthereumCapability first, then siweMessage will be computed
871
- if (showConnectDialog && authenticatedWalletAddress && signInWithEthereumCapability) {
872
- // siweMessage should be available since authenticatedWalletAddress is set
873
- if (!siweMessage) {
874
- // This shouldn't happen normally, but if SIWE message couldn't be built, fall through to ConnectDialog
875
- console.error('SIWE capability present but message could not be built');
876
- } else {
877
- return (
878
- <SiweDialog
879
- open={true}
880
- onOpenChange={(newOpen) => {
881
- if (!newOpen) handleSiweCancel();
882
- }}
883
- message={siweMessage}
884
- origin={origin}
885
- timestamp={new Date()}
886
- appName={request.data.appName || 'dApp'}
887
- appLogoUrl={request.data.appLogoUrl ?? undefined}
888
- accountAddress={authenticatedWalletAddress}
889
- chainName={chainName}
890
- chainId={targetChainId}
891
- chainIcon={chainIcon}
892
- mainnetRpcUrl={getMainnetRpcUrl(apiKey)}
893
- onSign={handleSiweSign}
894
- onCancel={handleSiweCancel}
895
- isProcessing={isSiweSigning}
896
- siweStatus={siweStatus}
897
- canSign={!isSiweSigning}
898
- />
899
- );
900
- }
901
- }
902
-
903
- // If showing ConnectDialog confirmation (no SIWE), render that instead
904
- if (showConnectDialog && authenticatedWalletAddress) {
905
- return (
906
- <ConnectDialog
907
- open={true}
908
- onOpenChange={(newOpen) => {
909
- if (!newOpen) handleConnectCancel();
910
- }}
911
- appName={request.data.appName || 'dApp'}
912
- appLogoUrl={request.data.appLogoUrl ?? undefined}
913
- origin={origin}
914
- timestamp={new Date()}
915
- accountName={authenticatedAccountName || 'Account'}
916
- walletAddress={authenticatedWalletAddress}
917
- chainName={chainName}
918
- chainId={targetChainId}
919
- chainIcon={chainIcon}
920
- mainnetRpcUrl={getMainnetRpcUrl(apiKey)}
921
- onConnect={async () => handleConnectConfirm()}
922
- onCancel={handleConnectCancel}
923
- showPermissions={false}
924
- isProcessing={isConnecting}
925
- />
926
- );
927
- }
928
-
929
- return (
930
- <DefaultDialogComponent
931
- open={open}
932
- onOpenChange={(newOpen) => {
933
- if (!newOpen) handleCancel();
934
- else setOpen(newOpen);
935
- }}
936
- handleClose={handleCancel}
937
- contentStyle={{
938
- width: 'fit-content',
939
- maxWidth: '450px',
940
- }}
941
- >
942
- <OnboardingDialog
943
- accounts={accounts}
944
- onAccountSelect={handleAccountSelect}
945
- loggingInAccount={loggingInAccount}
946
- onImportAccount={handleImportAccount}
947
- isImporting={isImporting}
948
- onCreateAccount={handleCreateAccount}
949
- onAccountCreationComplete={handleAccountCreationComplete}
950
- isCreating={isCreating}
951
- ensDomain={ensDomain}
952
- chainId={chainId}
953
- mainnetRpcUrl={getMainnetRpcUrl(apiKey)}
954
- apiKey={apiKey}
955
- supportedChains={SUPPORTED_CHAINS.map(chain => ({ id: chain.id }))}
956
- subnameTextRecords={subnameTextRecords}
957
- />
958
- </DefaultDialogComponent>
959
- );
960
- }
961
-
962
- function SignatureDialogWrapper({
963
- request,
964
- onApprove,
965
- onReject,
966
- apiKey,
967
- defaultChainId,
968
- paymasters,
969
- }: {
970
- request: SignatureUIRequest;
971
- onApprove: (data: any) => void;
972
- onReject: (error?: Error) => void;
973
- apiKey?: string;
974
- defaultChainId?: number;
975
- paymasters?: Record<number, PaymasterConfig>;
976
- }) {
977
- const [open, setOpen] = useState(true);
978
- const [isProcessing, setIsProcessing] = useState(false);
979
- const [signatureStatus, setSignatureStatus] = useState<string>('');
980
-
981
- // Use chainId from request (current chain), fallback to defaultChainId
982
- const chainId = request.data.chainId || defaultChainId || 1;
983
- const chainName = getChainNameFromId(chainId);
984
- const chainIcon = useChainIconURI(chainId, apiKey, 24);
985
-
986
- const handleSign = async () => {
987
- setIsProcessing(true);
988
- setSignatureStatus('Signing message...');
989
- try {
990
- // Restore account for signing
991
- const account = await getAccountForSigning(
992
- apiKey,
993
- chainId,
994
- paymasters?.[chainId]?.url
995
- );
996
-
997
- // Sign the message
998
- const signature = await account.signMessage(request.data.message);
999
-
1000
- setSignatureStatus('Signature successful!');
1001
- onApprove(signature);
1002
- } catch (error) {
1003
- console.error('Signature failed:', error);
1004
- const errorObj = error instanceof Error ? error : new Error(String(error));
1005
- setSignatureStatus(`Error: ${errorObj.message}`);
1006
- // Check if user cancelled passkey prompt (NotAllowedError)
1007
- if (errorObj.name === 'NotAllowedError') {
1008
- onReject(UIError.userRejected('User cancelled the passkey prompt'));
1009
- } else {
1010
- // Internal error
1011
- onReject(new UIError(standardErrorCodes.rpc.internal as UIErrorCode, errorObj.message));
1012
- }
1013
- } finally {
1014
- setIsProcessing(false);
1015
- }
1016
- };
1017
-
1018
- const handleCancel = () => {
1019
- setOpen(false);
1020
- onReject(UIError.userRejected());
1021
- };
1022
-
1023
- return (
1024
- <SignatureDialog
1025
- open={open}
1026
- onOpenChange={(newOpen) => {
1027
- if (!newOpen) handleCancel();
1028
- else setOpen(newOpen);
1029
- }}
1030
- message={request.data.message}
1031
- origin={typeof window !== 'undefined' ? window.location.origin : 'unknown'}
1032
- timestamp={new Date(request.timestamp)}
1033
- accountAddress={request.data.address}
1034
- chainName={chainName}
1035
- chainId={chainId}
1036
- chainIcon={chainIcon}
1037
- mainnetRpcUrl={getMainnetRpcUrl(apiKey)}
1038
- onSign={handleSign}
1039
- onCancel={handleCancel}
1040
- isProcessing={isProcessing}
1041
- signatureStatus={signatureStatus}
1042
- canSign={!isProcessing && !!request.data.message}
1043
- />
1044
- );
1045
- }
1046
-
1047
- function Eip712DialogWrapper({
1048
- request,
1049
- onApprove,
1050
- onReject,
1051
- apiKey,
1052
- defaultChainId,
1053
- paymasters,
1054
- }: {
1055
- request: TypedDataUIRequest;
1056
- onApprove: (data: any) => void;
1057
- onReject: (error?: Error) => void;
1058
- apiKey?: string;
1059
- defaultChainId?: number;
1060
- paymasters?: Record<number, PaymasterConfig>;
1061
- }) {
1062
- const [open, setOpen] = useState(true);
1063
- const [isProcessing, setIsProcessing] = useState(false);
1064
- const [signatureStatus, setSignatureStatus] = useState<string>('');
1065
-
1066
- // Use chainId from request (current chain), fallback to defaultChainId
1067
- const chainId = request.data.chainId || defaultChainId || 1;
1068
-
1069
- const handleSign = async () => {
1070
- setIsProcessing(true);
1071
- setSignatureStatus('Signing typed data...');
1072
- try {
1073
- // Restore account for signing
1074
- const account = await getAccountForSigning(
1075
- apiKey,
1076
- chainId,
1077
- paymasters?.[chainId]?.url
1078
- );
1079
-
1080
- // Parse typed data if it's a string
1081
- const typedData = typeof request.data.typedData === 'string'
1082
- ? JSON.parse(request.data.typedData)
1083
- : request.data.typedData;
1084
-
1085
- // Sign the typed data
1086
- const signature = await account.signTypedData({
1087
- domain: typedData.domain,
1088
- types: typedData.types,
1089
- primaryType: typedData.primaryType,
1090
- message: typedData.message,
1091
- });
1092
-
1093
- setSignatureStatus('Signature successful!');
1094
- onApprove(signature);
1095
- } catch (error) {
1096
- console.error('EIP-712 signature failed:', error);
1097
- const errorObj = error instanceof Error ? error : new Error(String(error));
1098
- setSignatureStatus(`Error: ${errorObj.message}`);
1099
- // Check if user cancelled passkey prompt (NotAllowedError)
1100
- if (errorObj.name === 'NotAllowedError') {
1101
- onReject(UIError.userRejected('User cancelled the passkey prompt'));
1102
- } else {
1103
- // Internal error
1104
- onReject(new UIError(standardErrorCodes.rpc.internal as UIErrorCode, errorObj.message));
1105
- }
1106
- } finally {
1107
- setIsProcessing(false);
1108
- }
1109
- };
1110
-
1111
- const handleCancel = () => {
1112
- setOpen(false);
1113
- onReject(UIError.userRejected());
1114
- };
1115
-
1116
- return (
1117
- <Eip712Dialog
1118
- open={open}
1119
- onOpenChange={(newOpen) => {
1120
- if (!newOpen) handleCancel();
1121
- else setOpen(newOpen);
1122
- }}
1123
- typedDataJson={request.data.typedData}
1124
- origin={typeof window !== 'undefined' ? window.location.origin : 'unknown'}
1125
- timestamp={new Date(request.timestamp)}
1126
- accountAddress={request.data.address}
1127
- mainnetRpcUrl={getMainnetRpcUrl(apiKey)}
1128
- onSign={handleSign}
1129
- onCancel={handleCancel}
1130
- isProcessing={isProcessing}
1131
- signatureStatus={signatureStatus}
1132
- canSign={true}
1133
- />
1134
- );
1135
- }
1136
-
1137
- function TransactionDialogWrapper({
1138
- request,
1139
- onApprove,
1140
- onReject,
1141
- apiKey,
1142
- defaultChainId,
1143
- paymasters,
1144
- }: {
1145
- request: TransactionUIRequest;
1146
- onApprove: (data: any) => void;
1147
- onReject: (error?: Error) => void;
1148
- apiKey?: string;
1149
- defaultChainId?: number;
1150
- paymasters?: Record<number, PaymasterConfig>;
1151
- }) {
1152
- const [open, setOpen] = useState(true);
1153
- const [isProcessing, setIsProcessing] = useState(false);
1154
- const [account, setAccount] = useState<Account | null>(null);
1155
- const [transactionStatus, setTransactionStatus] = useState<string>('');
1156
- // Fee token state for ERC-20 paymaster
1157
- const [feeTokens, setFeeTokens] = useState<FeeTokenOption[]>([]);
1158
- const [feeTokensLoading, setFeeTokensLoading] = useState(false);
1159
-
1160
- // Get native token symbol from feeTokens (defaults to ETH if not found)
1161
- const nativeToken = feeTokens?.find(t => t.isNative);
1162
- const nativeSymbol = nativeToken?.symbol || 'ETH';
1163
-
1164
- // Fetch native token price dynamically based on the chain's native token symbol
1165
- const nativeTokenPrice = useFeeTokenPrice(nativeSymbol);
1166
-
1167
- const chainId = request.data.chainId || defaultChainId || 1;
1168
- const viemChain = SUPPORTED_CHAINS.find(c => c.id === chainId);
1169
- const networkName = viemChain?.name || 'Unknown Network';
1170
-
1171
- // Extract paymasterUrl from capabilities (EIP-5792 paymasterService capability)
1172
- // Priority: capabilities.paymasterService.url > paymasters[chainId].url
1173
- const effectivePaymasterUrl = useMemo(() => {
1174
- const capabilitiesPaymasterUrl = request.data.capabilities?.paymasterService?.url;
1175
- return capabilitiesPaymasterUrl || paymasters?.[chainId]?.url;
1176
- }, [request.data.capabilities?.paymasterService?.url, paymasters, chainId]);
1177
-
1178
- // Extract paymasterContext from capabilities (for ERC-20 token payments, mode flags, etc.)
1179
- // Priority: capabilities.paymasterService.context > paymasters[chainId].context
1180
- const effectivePaymasterContext = useMemo(() => {
1181
- const capabilitiesPaymasterContext = (request.data.capabilities?.paymasterService as { context?: Record<string, unknown> } | undefined)?.context;
1182
- return capabilitiesPaymasterContext || paymasters?.[chainId]?.context;
1183
- }, [request.data.capabilities?.paymasterService, paymasters, chainId]);
1184
-
1185
- const isSponsored = !!effectivePaymasterUrl;
1186
-
1187
- // Transform calls to transactions format expected by dialog
1188
- const transactions = useMemo(() => request.data.calls.map(call => ({
1189
- to: call.to,
1190
- data: call.data,
1191
- value: call.value,
1192
- chainId: request.data.chainId,
1193
- })), [request.data.calls, request.data.chainId]);
1194
-
1195
- // Convert transactions to call format for Account operations
1196
- const transactionCalls = useMemo(() => {
1197
- return request.data.calls.map(call => ({
1198
- to: call.to as Address,
1199
- value: call.value ? BigInt(call.value) : undefined, // Convert string wei to bigint
1200
- data: (call.data || '0x') as Hex,
1201
- }));
1202
- }, [request.data.calls]);
1203
-
1204
- // Permission ID for permission-based execution
1205
- const permissionId = request.data.capabilities?.permissions?.id as Hex | undefined;
1206
-
1207
- // Use gas estimation hook for parallel ETH and ERC-20 estimation
1208
- const {
1209
- gasFee,
1210
- gasFeeLoading,
1211
- gasEstimationError,
1212
- tokenEstimates,
1213
- selectedFeeToken,
1214
- setSelectedFeeToken,
1215
- isPayingWithErc20,
1216
- } = useGasEstimation({
1217
- account,
1218
- transactionCalls,
1219
- chainId,
1220
- apiKey,
1221
- feeTokens,
1222
- isSponsored,
1223
- permissionId,
1224
- onFeeTokensUpdate: setFeeTokens,
1225
- });
1226
-
1227
- // Compute paymaster URL based on fee token selection (for ERC-20 paymaster)
1228
- const computedPaymasterUrl = useMemo(() => {
1229
- // If already sponsored via capabilities or config, use that
1230
- if (effectivePaymasterUrl) return effectivePaymasterUrl;
1231
-
1232
- // If user selected an ERC-20 token (non-native), use ERC-20 paymaster
1233
- if (selectedFeeToken && !selectedFeeToken.isNative) {
1234
- return `${JAW_PAYMASTER_URL}?chainId=${chainId}${apiKey ? `&api-key=${apiKey}` : ''}`;
1235
- }
1236
-
1237
- // Native ETH - no paymaster needed
1238
- return undefined;
1239
- }, [effectivePaymasterUrl, selectedFeeToken, chainId, apiKey]);
1240
-
1241
- // Compute paymaster context based on fee token selection
1242
- const computedPaymasterContext = useMemo(() => {
1243
- // If using ERC-20 paymaster, include token address and gas amount in context
1244
- if (selectedFeeToken && !selectedFeeToken.isNative) {
1245
- // Use the actual estimate from tokenEstimates if available
1246
- const estimate = tokenEstimates.find(
1247
- e => e.tokenAddress.toLowerCase() === selectedFeeToken.address.toLowerCase()
1248
- );
1249
-
1250
- if (estimate) {
1251
- // Use the actual token cost from paymaster quote
1252
- return {
1253
- token: selectedFeeToken.address,
1254
- gas: estimate.tokenCost.toString(),
1255
- };
1256
- }
1257
-
1258
- // Fallback to client-side calculation if no estimate yet
1259
- const gasUsd = gasFee && nativeTokenPrice ? nativeTokenPrice * Number(gasFee) : 0;
1260
- const gasInTokenUnits = Math.ceil(gasUsd * Math.pow(10, selectedFeeToken.decimals));
1261
- return {
1262
- token: selectedFeeToken.address,
1263
- gas: gasInTokenUnits.toString(),
1264
- };
1265
- }
1266
- return effectivePaymasterContext;
1267
- }, [selectedFeeToken, effectivePaymasterContext, gasFee, nativeTokenPrice, tokenEstimates]);
1268
-
1269
- // Fetch fee tokens when not sponsored (for ERC-20 paymaster option)
1270
- useEffect(() => {
1271
- // Skip if already sponsored via capabilities or config
1272
- if (effectivePaymasterUrl) return;
1273
-
1274
- let isMounted = true;
1275
-
1276
- const fetchFeeTokensData = async () => {
1277
- setFeeTokensLoading(true);
1278
- try {
1279
- // Fetch capabilities from JAW RPC
1280
- const capabilities = await handleGetCapabilitiesRequest(
1281
- { method: 'wallet_getCapabilities', params: [] },
1282
- apiKey || '',
1283
- true // showTestnets
1284
- );
1285
-
1286
- const chainIdHex = `0x${chainId.toString(16)}` as `0x${string}`;
1287
- const feeTokenCap = capabilities?.[chainIdHex]?.feeToken as FeeTokenCapability | undefined;
1288
-
1289
- if (!feeTokenCap?.supported || !feeTokenCap?.tokens?.length) {
1290
- if (isMounted) setFeeTokensLoading(false);
1291
- return;
1292
- }
1293
-
1294
- // Get RPC URL for balance fetching
1295
- const rpcUrl = viemChain?.rpcUrls?.default?.http?.[0] || `https://eth.llamarpc.com`;
1296
-
1297
- // Fetch balances in parallel
1298
- const tokensWithBalances = await Promise.all(
1299
- feeTokenCap.tokens.map(async (token) => {
1300
- try {
1301
- const balance = await fetchTokenBalance(token.address, request.data.from, rpcUrl);
1302
- const balanceFormatted = formatUnits(balance, token.decimals);
1303
- const isNative = isNativeToken(token.address);
1304
- // For native token (ETH): selectable if any balance (gas estimation will catch insufficient)
1305
- // For ERC-20 tokens: require at least 0.5 units
1306
- const isSelectable = isNative
1307
- ? balance > 0n
1308
- : parseFloat(balanceFormatted) >= 0.5;
1309
-
1310
- return {
1311
- uid: token.uid,
1312
- symbol: token.symbol,
1313
- address: token.address,
1314
- decimals: token.decimals,
1315
- balance,
1316
- balanceFormatted,
1317
- isNative,
1318
- isSelectable,
1319
- logoURI: token.logoURI,
1320
- } as FeeTokenOption;
1321
- } catch (error) {
1322
- console.warn(`Failed to fetch balance for ${token.symbol}:`, error);
1323
- return {
1324
- uid: token.uid,
1325
- symbol: token.symbol,
1326
- address: token.address,
1327
- decimals: token.decimals,
1328
- balance: 0n,
1329
- balanceFormatted: '0',
1330
- isNative: isNativeToken(token.address),
1331
- isSelectable: false,
1332
- logoURI: token.logoURI,
1333
- } as FeeTokenOption;
1334
- }
1335
- })
1336
- );
1337
-
1338
- if (isMounted) {
1339
- setFeeTokens(tokensWithBalances);
1340
- // Note: Initial token selection is handled by useGasEstimation hook
1341
- }
1342
- } catch (error) {
1343
- console.warn('[TransactionDialogWrapper] Failed to fetch fee tokens:', error);
1344
- } finally {
1345
- if (isMounted) setFeeTokensLoading(false);
1346
- }
1347
- };
1348
-
1349
- fetchFeeTokensData();
1350
-
1351
- return () => {
1352
- isMounted = false;
1353
- };
1354
- }, [chainId, apiKey, request.data.from, effectivePaymasterUrl, viemChain]);
1355
-
1356
- // Initialize account
1357
- // Note: Use effectivePaymasterUrl (stable) instead of computedPaymasterUrl to avoid
1358
- // re-initializing account when user changes fee token selection (which would cause
1359
- // gas estimation to run multiple times in a dependency cycle)
1360
- useEffect(() => {
1361
- let isMounted = true;
1362
-
1363
- const initializeAccount = async () => {
1364
- try {
1365
- const restoredAccount = await getAccountForSigning(
1366
- apiKey,
1367
- chainId,
1368
- effectivePaymasterUrl
1369
- );
1370
- if (isMounted) {
1371
- setAccount(restoredAccount);
1372
- }
1373
- } catch (error) {
1374
- console.error('Error initializing account:', error);
1375
- }
1376
- };
1377
-
1378
- initializeAccount();
1379
-
1380
- return () => {
1381
- isMounted = false;
1382
- };
1383
- }, [apiKey, chainId, effectivePaymasterUrl]);
1384
-
1385
- // Note: Gas estimation is now handled by useGasEstimation hook
1386
-
1387
- const handleConfirm = async () => {
1388
- setIsProcessing(true);
1389
- setTransactionStatus('Processing transaction...');
1390
- try {
1391
- if (!account) {
1392
- throw new Error('Account not initialized');
1393
- }
1394
-
1395
- // Check if permissions capability is provided
1396
- const permissionId = request.data.capabilities?.permissions?.id;
1397
-
1398
- const result = await account.sendCalls(
1399
- transactionCalls,
1400
- permissionId ? { permissionId } : undefined,
1401
- computedPaymasterUrl,
1402
- computedPaymasterContext
1403
- );
1404
-
1405
- setTransactionStatus('Transaction successful!');
1406
- onApprove({
1407
- id: result.id,
1408
- chainId: result.chainId,
1409
- });
1410
- } catch (error) {
1411
- console.error('Transaction failed:', error);
1412
- const errorObj = error instanceof Error ? error : new Error(String(error));
1413
- const errorMessage = errorObj.message;
1414
- setTransactionStatus(`Error: ${errorMessage}`);
1415
- // Check if user cancelled passkey prompt (NotAllowedError)
1416
- if (errorObj.name === 'NotAllowedError') {
1417
- onReject(UIError.userRejected('User cancelled the passkey prompt'));
1418
- } else if (
1419
- errorMessage.includes('AA21') ||
1420
- errorMessage.includes("didn't pay prefund") ||
1421
- errorMessage.includes('insufficient') ||
1422
- errorMessage.includes('exceeds balance')
1423
- ) {
1424
- // Transaction rejected due to funds/gas issues
1425
- onReject(new UIError(standardErrorCodes.rpc.transactionRejected as UIErrorCode, errorMessage));
1426
- } else {
1427
- // Internal error
1428
- onReject(new UIError(standardErrorCodes.rpc.internal as UIErrorCode, errorMessage));
1429
- }
1430
- } finally {
1431
- setIsProcessing(false);
1432
- }
1433
- };
1434
-
1435
- const handleCancel = () => {
1436
- setOpen(false);
1437
- onReject(UIError.userRejected());
1438
- };
1439
-
1440
- // Determine if fee token selector should be shown (not sponsored and has ERC-20 options)
1441
- const showFeeTokenSelector = !isSponsored && feeTokens.some(t => !t.isNative);
1442
-
1443
- return (
1444
- <TransactionDialog
1445
- open={open}
1446
- onOpenChange={(newOpen) => {
1447
- if (!newOpen) handleCancel();
1448
- else setOpen(newOpen);
1449
- }}
1450
- transactions={transactions}
1451
- walletAddress={request.data.from}
1452
- gasFee={gasFee}
1453
- gasFeeLoading={gasFeeLoading}
1454
- gasEstimationError={gasEstimationError}
1455
- sponsored={isSponsored}
1456
- onConfirm={handleConfirm}
1457
- onCancel={handleCancel}
1458
- isProcessing={isProcessing}
1459
- transactionStatus={transactionStatus}
1460
- networkName={networkName}
1461
- mainnetRpcUrl={getMainnetRpcUrl(apiKey)}
1462
- apiKey={apiKey}
1463
- // Fee token props for ERC-20 paymaster
1464
- feeTokens={feeTokens}
1465
- feeTokensLoading={feeTokensLoading}
1466
- selectedFeeToken={selectedFeeToken}
1467
- onFeeTokenSelect={setSelectedFeeToken}
1468
- showFeeTokenSelector={showFeeTokenSelector}
1469
- isPayingWithErc20={isPayingWithErc20}
1470
- />
1471
- );
1472
- }
1473
-
1474
- // SendTransactionDialogWrapper - handles eth_sendTransaction (legacy single transaction)
1475
- function SendTransactionDialogWrapper({
1476
- request,
1477
- onApprove,
1478
- onReject,
1479
- apiKey,
1480
- defaultChainId,
1481
- paymasters,
1482
- }: {
1483
- request: SendTransactionUIRequest;
1484
- onApprove: (data: any) => void;
1485
- onReject: (error?: Error) => void;
1486
- apiKey?: string;
1487
- defaultChainId?: number;
1488
- paymasters?: Record<number, PaymasterConfig>;
1489
- }) {
1490
- const [open, setOpen] = useState(true);
1491
- const [isProcessing, setIsProcessing] = useState(false);
1492
- const [account, setAccount] = useState<Account | null>(null);
1493
- const [transactionStatus, setTransactionStatus] = useState<string>('');
1494
- // Fee token state for ERC-20 paymaster
1495
- const [feeTokens, setFeeTokens] = useState<FeeTokenOption[]>([]);
1496
- const [feeTokensLoading, setFeeTokensLoading] = useState(false);
1497
-
1498
- // Get native token symbol from feeTokens (defaults to ETH if not found)
1499
- const nativeToken = feeTokens?.find(t => t.isNative);
1500
- const nativeSymbol = nativeToken?.symbol || 'ETH';
1501
-
1502
- // Fetch native token price dynamically based on the chain's native token symbol
1503
- const nativeTokenPrice = useFeeTokenPrice(nativeSymbol);
1504
-
1505
- const chainId = request.data.chainId || defaultChainId || 1;
1506
- const viemChain = SUPPORTED_CHAINS.find(c => c.id === chainId);
1507
- const networkName = viemChain?.name || 'Unknown Network';
1508
-
1509
- // Extract paymasterUrl from capabilities (EIP-5792 paymasterService capability)
1510
- // Priority: capabilities.paymasterService.url > paymasters[chainId].url
1511
- const effectivePaymasterUrl = useMemo(() => {
1512
- const capabilitiesPaymasterUrl = request.data.capabilities?.paymasterService?.url;
1513
- return capabilitiesPaymasterUrl || paymasters?.[chainId]?.url;
1514
- }, [request.data.capabilities?.paymasterService?.url, paymasters, chainId]);
1515
-
1516
- // Extract paymasterContext from capabilities (for ERC-20 token payments, mode flags, etc.)
1517
- // Priority: capabilities.paymasterService.context > paymasters[chainId].context
1518
- const effectivePaymasterContext = useMemo(() => {
1519
- const capabilitiesPaymasterContext = (request.data.capabilities?.paymasterService as { context?: Record<string, unknown> } | undefined)?.context;
1520
- return capabilitiesPaymasterContext || paymasters?.[chainId]?.context;
1521
- }, [request.data.capabilities?.paymasterService, paymasters, chainId]);
1522
-
1523
- const isSponsored = !!effectivePaymasterUrl;
1524
-
1525
- // Transform eth_sendTransaction data to transactions format expected by dialog
1526
- const transactions = useMemo(() => [{
1527
- to: request.data.to,
1528
- data: request.data.data,
1529
- value: request.data.value,
1530
- chainId: request.data.chainId,
1531
- }], [request.data]);
1532
-
1533
- // Convert to call format for Account operations
1534
- const transactionCalls = useMemo(() => [{
1535
- to: request.data.to as Address,
1536
- value: request.data.value ? BigInt(request.data.value) : undefined, // Convert string wei to bigint
1537
- data: (request.data.data || '0x') as Hex,
1538
- }], [request.data]);
1539
-
1540
- // Use gas estimation hook for parallel ETH and ERC-20 estimation
1541
- const {
1542
- gasFee,
1543
- gasFeeLoading,
1544
- gasEstimationError,
1545
- tokenEstimates,
1546
- selectedFeeToken,
1547
- setSelectedFeeToken,
1548
- isPayingWithErc20,
1549
- } = useGasEstimation({
1550
- account,
1551
- transactionCalls,
1552
- chainId,
1553
- apiKey,
1554
- feeTokens,
1555
- isSponsored,
1556
- onFeeTokensUpdate: setFeeTokens,
1557
- });
1558
-
1559
- // Compute paymaster URL based on fee token selection (for ERC-20 paymaster)
1560
- const computedPaymasterUrl = useMemo(() => {
1561
- // If already sponsored via capabilities or config, use that
1562
- if (effectivePaymasterUrl) return effectivePaymasterUrl;
1563
-
1564
- // If user selected an ERC-20 token (non-native), use ERC-20 paymaster
1565
- if (selectedFeeToken && !selectedFeeToken.isNative) {
1566
- return `${JAW_PAYMASTER_URL}?chainId=${chainId}${apiKey ? `&api-key=${apiKey}` : ''}`;
1567
- }
1568
-
1569
- // Native ETH - no paymaster needed
1570
- return undefined;
1571
- }, [effectivePaymasterUrl, selectedFeeToken, chainId, apiKey]);
1572
-
1573
- // Compute paymaster context based on fee token selection
1574
- const computedPaymasterContext = useMemo(() => {
1575
- // If using ERC-20 paymaster, include token address and gas amount in context
1576
- if (selectedFeeToken && !selectedFeeToken.isNative) {
1577
- // Use the actual estimate from tokenEstimates if available
1578
- const estimate = tokenEstimates.find(
1579
- e => e.tokenAddress.toLowerCase() === selectedFeeToken.address.toLowerCase()
1580
- );
1581
-
1582
- if (estimate) {
1583
- // Use the actual token cost from paymaster quote
1584
- return {
1585
- token: selectedFeeToken.address,
1586
- gas: estimate.tokenCost.toString(),
1587
- };
1588
- }
1589
-
1590
- // Fallback to client-side calculation if no estimate yet
1591
- const gasUsd = gasFee && nativeTokenPrice ? nativeTokenPrice * Number(gasFee) : 0;
1592
- const gasInTokenUnits = Math.ceil(gasUsd * Math.pow(10, selectedFeeToken.decimals));
1593
- return {
1594
- token: selectedFeeToken.address,
1595
- gas: gasInTokenUnits.toString(),
1596
- };
1597
- }
1598
- return effectivePaymasterContext;
1599
- }, [selectedFeeToken, effectivePaymasterContext, gasFee, nativeTokenPrice, tokenEstimates]);
1600
-
1601
- // Fetch fee tokens when not sponsored (for ERC-20 paymaster option)
1602
- useEffect(() => {
1603
- // Skip if already sponsored via capabilities or config
1604
- if (effectivePaymasterUrl) return;
1605
-
1606
- let isMounted = true;
1607
-
1608
- const fetchFeeTokensData = async () => {
1609
- setFeeTokensLoading(true);
1610
- try {
1611
- // Fetch capabilities from JAW RPC
1612
- const capabilities = await handleGetCapabilitiesRequest(
1613
- { method: 'wallet_getCapabilities', params: [] },
1614
- apiKey || '',
1615
- true // showTestnets
1616
- );
1617
-
1618
- const chainIdHex = `0x${chainId.toString(16)}` as `0x${string}`;
1619
- const feeTokenCap = capabilities?.[chainIdHex]?.feeToken as FeeTokenCapability | undefined;
1620
-
1621
- if (!feeTokenCap?.supported || !feeTokenCap?.tokens?.length) {
1622
- if (isMounted) setFeeTokensLoading(false);
1623
- return;
1624
- }
1625
-
1626
- // Get RPC URL for balance fetching
1627
- const rpcUrl = viemChain?.rpcUrls?.default?.http?.[0] || `https://eth.llamarpc.com`;
1628
-
1629
- // Fetch balances in parallel
1630
- const tokensWithBalances = await Promise.all(
1631
- feeTokenCap.tokens.map(async (token) => {
1632
- try {
1633
- const balance = await fetchTokenBalance(token.address, request.data.from, rpcUrl);
1634
- const balanceFormatted = formatUnits(balance, token.decimals);
1635
- const isNative = isNativeToken(token.address);
1636
- // For native token (ETH): selectable if any balance (gas estimation will catch insufficient)
1637
- // For ERC-20 tokens: require at least 0.5 units
1638
- const isSelectable = isNative
1639
- ? balance > 0n
1640
- : parseFloat(balanceFormatted) >= 0.5;
1641
-
1642
- return {
1643
- uid: token.uid,
1644
- symbol: token.symbol,
1645
- address: token.address,
1646
- decimals: token.decimals,
1647
- balance,
1648
- balanceFormatted,
1649
- isNative,
1650
- isSelectable,
1651
- logoURI: token.logoURI,
1652
- } as FeeTokenOption;
1653
- } catch (error) {
1654
- console.warn(`Failed to fetch balance for ${token.symbol}:`, error);
1655
- return {
1656
- uid: token.uid,
1657
- symbol: token.symbol,
1658
- address: token.address,
1659
- decimals: token.decimals,
1660
- balance: 0n,
1661
- balanceFormatted: '0',
1662
- isNative: isNativeToken(token.address),
1663
- isSelectable: false,
1664
- logoURI: token.logoURI,
1665
- } as FeeTokenOption;
1666
- }
1667
- })
1668
- );
1669
-
1670
- if (isMounted) {
1671
- setFeeTokens(tokensWithBalances);
1672
- // Note: Initial token selection is handled by useGasEstimation hook
1673
- }
1674
- } catch (error) {
1675
- console.warn('[SendTransactionDialogWrapper] Failed to fetch fee tokens:', error);
1676
- } finally {
1677
- if (isMounted) setFeeTokensLoading(false);
1678
- }
1679
- };
1680
-
1681
- fetchFeeTokensData();
1682
-
1683
- return () => {
1684
- isMounted = false;
1685
- };
1686
- }, [chainId, apiKey, request.data.from, effectivePaymasterUrl, viemChain]);
1687
-
1688
- // Initialize account
1689
- // Note: Use effectivePaymasterUrl (stable) instead of computedPaymasterUrl to avoid
1690
- // re-initializing account when user changes fee token selection (which would cause
1691
- // gas estimation to run multiple times in a dependency cycle)
1692
- useEffect(() => {
1693
- let isMounted = true;
1694
-
1695
- const initializeAccount = async () => {
1696
- try {
1697
- const restoredAccount = await getAccountForSigning(
1698
- apiKey,
1699
- chainId,
1700
- effectivePaymasterUrl
1701
- );
1702
- if (isMounted) {
1703
- setAccount(restoredAccount);
1704
- }
1705
- } catch (error) {
1706
- console.error('Error initializing account:', error);
1707
- }
1708
- };
1709
-
1710
- initializeAccount();
1711
-
1712
- return () => {
1713
- isMounted = false;
1714
- };
1715
- }, [apiKey, chainId, effectivePaymasterUrl]);
1716
-
1717
- // Note: Gas estimation is now handled by useGasEstimation hook
1718
-
1719
- const handleConfirm = async () => {
1720
- setIsProcessing(true);
1721
- setTransactionStatus('Processing transaction...');
1722
- try {
1723
- if (!account) {
1724
- throw new Error('Account not initialized');
1725
- }
1726
-
1727
- const txHash = await account.sendTransaction(transactionCalls, computedPaymasterUrl, computedPaymasterContext);
1728
-
1729
- setTransactionStatus('Transaction successful!');
1730
- onApprove(txHash);
1731
- } catch (error) {
1732
- console.error('Transaction failed:', error);
1733
- const errorObj = error instanceof Error ? error : new Error(String(error));
1734
- const errorMessage = errorObj.message;
1735
- setTransactionStatus(`Error: ${errorMessage}`);
1736
- // Check if user cancelled passkey prompt (NotAllowedError)
1737
- if (errorObj.name === 'NotAllowedError') {
1738
- onReject(UIError.userRejected('User cancelled the passkey prompt'));
1739
- } else if (
1740
- errorMessage.includes('AA21') ||
1741
- errorMessage.includes("didn't pay prefund") ||
1742
- errorMessage.includes('insufficient') ||
1743
- errorMessage.includes('exceeds balance')
1744
- ) {
1745
- // Transaction rejected due to funds/gas issues
1746
- onReject(new UIError(standardErrorCodes.rpc.transactionRejected as UIErrorCode, errorMessage));
1747
- } else {
1748
- // Internal error
1749
- onReject(new UIError(standardErrorCodes.rpc.internal as UIErrorCode, errorMessage));
1750
- }
1751
- } finally {
1752
- setIsProcessing(false);
1753
- }
1754
- };
1755
-
1756
- const handleCancel = () => {
1757
- setOpen(false);
1758
- onReject(UIError.userRejected());
1759
- };
1760
-
1761
- // Determine if fee token selector should be shown (not sponsored and has ERC-20 options)
1762
- const showFeeTokenSelector = !isSponsored && feeTokens.some(t => !t.isNative);
1763
-
1764
- return (
1765
- <TransactionDialog
1766
- open={open}
1767
- onOpenChange={(newOpen) => {
1768
- if (!newOpen) handleCancel();
1769
- else setOpen(newOpen);
1770
- }}
1771
- transactions={transactions}
1772
- walletAddress={request.data.from}
1773
- gasFee={gasFee}
1774
- gasFeeLoading={gasFeeLoading}
1775
- gasEstimationError={gasEstimationError}
1776
- sponsored={isSponsored}
1777
- onConfirm={handleConfirm}
1778
- onCancel={handleCancel}
1779
- isProcessing={isProcessing}
1780
- transactionStatus={transactionStatus}
1781
- networkName={networkName}
1782
- mainnetRpcUrl={getMainnetRpcUrl(apiKey)}
1783
- apiKey={apiKey}
1784
- // Fee token props for ERC-20 paymaster
1785
- feeTokens={feeTokens}
1786
- feeTokensLoading={feeTokensLoading}
1787
- selectedFeeToken={selectedFeeToken}
1788
- onFeeTokenSelect={setSelectedFeeToken}
1789
- showFeeTokenSelector={showFeeTokenSelector}
1790
- isPayingWithErc20={isPayingWithErc20}
1791
- />
1792
- );
1793
- }
1794
-
1795
- // Known function selectors mapping
1796
- const KNOWN_FUNCTION_SELECTORS: Record<string, string> = {
1797
- '0x32323232': 'Any Function',
1798
- '0xe0e0e0e0': 'Empty Calldata',
1799
- '0xcc53287f': 'lockdown((address,address)[])',
1800
- '0x87517c45': 'approve(address,address,uint160,uint48)',
1801
- '0x095ea7b3': 'approve(address,uint256)',
1802
- '0x23b872dd': 'transferFrom(address,address,uint256)',
1803
- '0xa9059cbb': 'transfer(address,uint256)',
1804
- };
1805
-
1806
- // Resolve function selector to human-readable name
1807
- const resolveFunctionSelector = (selector: string): string => {
1808
- const normalizedSelector = selector.toLowerCase();
1809
- const knownName = KNOWN_FUNCTION_SELECTORS[normalizedSelector];
1810
- return knownName || selector;
1811
- };
1812
-
1813
- function PermissionDialogWrapper({
1814
- request,
1815
- onApprove,
1816
- onReject,
1817
- apiKey,
1818
- defaultChainId,
1819
- paymasters,
1820
- }: {
1821
- request: PermissionUIRequest;
1822
- onApprove: (data: any) => void;
1823
- onReject: (error?: Error) => void;
1824
- apiKey?: string;
1825
- defaultChainId?: number;
1826
- paymasters?: Record<number, PaymasterConfig>;
1827
- }) {
1828
- const [open, setOpen] = useState(true);
1829
- const [isProcessing, setIsProcessing] = useState(false);
1830
- const [status, setStatus] = useState<string>('');
1831
- const [tokenInfoMap, setTokenInfoMap] = useState<TokenInfoMap>({});
1832
- const [isLoadingTokenInfo, setIsLoadingTokenInfo] = useState(true);
1833
- const [account, setAccount] = useState<Account | null>(null);
1834
- const [feeTokens, setFeeTokens] = useState<FeeTokenOption[]>([]);
1835
- const [feeTokensLoading, setFeeTokensLoading] = useState(true);
1836
-
1837
- // Get native token symbol from feeTokens (defaults to ETH if not found)
1838
- const nativeToken = feeTokens?.find(t => t.isNative);
1839
- const nativeSymbol = nativeToken?.symbol || 'ETH';
1840
-
1841
- // Fetch native token price dynamically based on the chain's native token symbol
1842
- const nativeTokenPrice = useFeeTokenPrice(nativeSymbol);
1843
-
1844
- // chainId can be number or hex string (like '0x1')
1845
- const requestChainId = request.data.chainId;
1846
- const chainId = typeof requestChainId === 'string'
1847
- ? parseInt(requestChainId, requestChainId.startsWith('0x') ? 16 : 10)
1848
- : (requestChainId || defaultChainId || 1);
1849
- const viemChain = SUPPORTED_CHAINS.find(c => c.id === chainId);
1850
- const networkName = viemChain?.name || 'Unknown Network';
1851
-
1852
- // Extract paymasterUrl from capabilities (EIP-5792 paymasterService capability)
1853
- // Priority: capabilities.paymasterService.url > paymasters[chainId].url
1854
- const effectivePaymasterUrl = useMemo(() => {
1855
- const capabilitiesPaymasterUrl = request.data.capabilities?.paymasterService?.url;
1856
- return capabilitiesPaymasterUrl || paymasters?.[chainId]?.url;
1857
- }, [request.data.capabilities?.paymasterService?.url, paymasters, chainId]);
1858
-
1859
- // Extract paymasterContext from capabilities (for ERC-20 token payments, mode flags, etc.)
1860
- // Priority: capabilities.paymasterService.context > paymasters[chainId].context
1861
- const effectivePaymasterContext = useMemo(() => {
1862
- const capabilitiesPaymasterContext = (request.data.capabilities?.paymasterService as { context?: Record<string, unknown> } | undefined)?.context;
1863
- return capabilitiesPaymasterContext || paymasters?.[chainId]?.context;
1864
- }, [request.data.capabilities?.paymasterService, paymasters, chainId]);
1865
-
1866
- // Check if this is a sponsored transaction (paymaster provided)
1867
- const isSponsored = !!effectivePaymasterUrl;
1868
-
1869
- // Build the actual permission grant call for gas estimation
1870
- // This uses the real approve() call data to PERMISSIONS_MANAGER_ADDRESS
1871
- const transactionCalls = useMemo(() => {
1872
- // Need account address to build the call - will be empty until account is initialized
1873
- if (!request.data.address) return [];
1874
-
1875
- try {
1876
- const permissionCall = buildGrantPermissionCall(
1877
- request.data.address as Address,
1878
- request.data.spender as Address,
1879
- request.data.expiry,
1880
- request.data.permissions
1881
- );
1882
- return [permissionCall];
1883
- } catch (error) {
1884
- console.warn('[PermissionDialogWrapper] Failed to build permission grant call:', error);
1885
- return [];
1886
- }
1887
- }, [request.data.address, request.data.spender, request.data.expiry, request.data.permissions]);
1888
-
1889
- // Use the gas estimation hook for both ETH and ERC-20 cost estimation
1890
- const {
1891
- gasFee,
1892
- gasFeeLoading,
1893
- gasEstimationError,
1894
- tokenEstimates,
1895
- selectedFeeToken,
1896
- setSelectedFeeToken,
1897
- isPayingWithErc20,
1898
- } = useGasEstimation({
1899
- account,
1900
- transactionCalls,
1901
- chainId,
1902
- apiKey,
1903
- feeTokens,
1904
- isSponsored,
1905
- onFeeTokensUpdate: setFeeTokens,
1906
- });
1907
-
1908
- // Compute paymaster URL based on fee token selection (for ERC-20 paymaster)
1909
- const computedPaymasterUrl = useMemo(() => {
1910
- // If already sponsored via capabilities or config, use that
1911
- if (effectivePaymasterUrl) return effectivePaymasterUrl;
1912
-
1913
- // If user selected an ERC-20 token (non-native), use ERC-20 paymaster
1914
- if (selectedFeeToken && !selectedFeeToken.isNative) {
1915
- return `${JAW_PAYMASTER_URL}?chainId=${chainId}${apiKey ? `&api-key=${apiKey}` : ''}`;
1916
- }
1917
-
1918
- // Native ETH - no paymaster needed
1919
- return undefined;
1920
- }, [effectivePaymasterUrl, selectedFeeToken, chainId, apiKey]);
1921
-
1922
- // Compute paymaster context based on fee token selection
1923
- const computedPaymasterContext = useMemo(() => {
1924
- // If using ERC-20 paymaster, include token address and gas amount in context
1925
- if (selectedFeeToken && !selectedFeeToken.isNative) {
1926
- // Use the actual estimate from tokenEstimates if available
1927
- const estimate = tokenEstimates.find(
1928
- e => e.tokenAddress.toLowerCase() === selectedFeeToken.address.toLowerCase()
1929
- );
1930
-
1931
- if (estimate) {
1932
- // Use the actual token cost from paymaster quote
1933
- return {
1934
- token: selectedFeeToken.address,
1935
- gas: estimate.tokenCost.toString(),
1936
- };
1937
- }
1938
-
1939
- // Fallback to client-side calculation if no estimate yet
1940
- const gasUsd = gasFee && nativeTokenPrice ? nativeTokenPrice * Number(gasFee) : 0;
1941
- const gasInTokenUnits = Math.ceil(gasUsd * Math.pow(10, selectedFeeToken.decimals));
1942
- return {
1943
- token: selectedFeeToken.address,
1944
- gas: gasInTokenUnits.toString(),
1945
- };
1946
- }
1947
- return effectivePaymasterContext;
1948
- }, [selectedFeeToken, effectivePaymasterContext, tokenEstimates, gasFee, nativeTokenPrice]);
1949
-
1950
- const chain = useMemo(
1951
- () => buildChainConfigFromApiKey(chainId, apiKey, computedPaymasterUrl),
1952
- [chainId, apiKey, computedPaymasterUrl]
1953
- );
1954
-
1955
- // Get spends array from request (now using spends plural)
1956
- const spendsData = request.data.permissions.spends || [];
1957
-
1958
- // Get calls array from request
1959
- const callsData = request.data.permissions.calls || [];
1960
-
1961
- // Fetch token info for all unique tokens in spends
1962
- useEffect(() => {
1963
- if (spendsData.length === 0) {
1964
- setIsLoadingTokenInfo(false);
1965
- return;
1966
- }
1967
-
1968
- let isMounted = true;
1969
- setIsLoadingTokenInfo(true);
1970
-
1971
- const fetchAllTokenInfo = async () => {
1972
- const newTokenInfoMap: TokenInfoMap = {};
1973
-
1974
- // Get unique token addresses
1975
- const uniqueTokens = Array.from(new Set(spendsData.map((spend) => spend.token))) as string[];
1976
-
1977
- for (const tokenAddress of uniqueTokens) {
1978
- // Skip if already fetched
1979
- if (tokenInfoMap[tokenAddress]) {
1980
- newTokenInfoMap[tokenAddress] = tokenInfoMap[tokenAddress];
1981
- continue;
1982
- }
1983
-
1984
- // If native token, use chain's native currency
1985
- if (isNativeToken(tokenAddress)) {
1986
- newTokenInfoMap[tokenAddress] = {
1987
- decimals: viemChain?.nativeCurrency?.decimals ?? 18,
1988
- symbol: viemChain?.nativeCurrency?.symbol || 'ETH'
1989
- };
1990
- continue;
1991
- }
1992
-
1993
- // Fetch ERC-20 token info
1994
- try {
1995
- const publicClient = createPublicClient({
1996
- chain: {
1997
- id: chainId,
1998
- name: networkName,
1999
- nativeCurrency: viemChain?.nativeCurrency || { name: 'Ether', symbol: 'ETH', decimals: 18 },
2000
- rpcUrls: {
2001
- default: { http: [chain.rpcUrl || ''] },
2002
- public: { http: [chain.rpcUrl || ''] },
2003
- },
2004
- },
2005
- transport: http(chain.rpcUrl),
2006
- });
2007
-
2008
- const [decimals, symbol] = await Promise.all([
2009
- publicClient.readContract({
2010
- address: tokenAddress as Address,
2011
- abi: erc20Abi,
2012
- functionName: 'decimals',
2013
- }),
2014
- publicClient.readContract({
2015
- address: tokenAddress as Address,
2016
- abi: erc20Abi,
2017
- functionName: 'symbol',
2018
- }),
2019
- ]);
2020
-
2021
- newTokenInfoMap[tokenAddress] = { decimals, symbol };
2022
- } catch (error) {
2023
- console.error(`Failed to fetch token info for ${tokenAddress}:`, error);
2024
- // Fallback to showing truncated token address
2025
- newTokenInfoMap[tokenAddress] = {
2026
- decimals: 18,
2027
- symbol: tokenAddress.slice(0, 6) + '...' + tokenAddress.slice(-4),
2028
- };
2029
- }
2030
- }
2031
-
2032
- if (isMounted) {
2033
- setTokenInfoMap(prev => ({ ...prev, ...newTokenInfoMap }));
2034
- setIsLoadingTokenInfo(false);
2035
- }
2036
- };
2037
-
2038
- fetchAllTokenInfo();
2039
-
2040
- return () => {
2041
- isMounted = false;
2042
- };
2043
- }, [chainId, spendsData, networkName, chain.rpcUrl, viemChain]);
2044
-
2045
- // Fetch fee tokens from capabilities (same pattern as TransactionDialogWrapper)
2046
- useEffect(() => {
2047
- let isMounted = true;
2048
-
2049
- const fetchFeeTokensData = async () => {
2050
- if (!viemChain || !apiKey) {
2051
- setFeeTokensLoading(false);
2052
- return;
2053
- }
2054
-
2055
- // If sponsored, no need to fetch fee tokens
2056
- if (effectivePaymasterUrl) {
2057
- setFeeTokensLoading(false);
2058
- return;
2059
- }
2060
-
2061
- try {
2062
- setFeeTokensLoading(true);
2063
-
2064
- // Fetch capabilities from JAW RPC
2065
- const capabilities = await handleGetCapabilitiesRequest(
2066
- { method: 'wallet_getCapabilities', params: [] },
2067
- apiKey || '',
2068
- true // showTestnets
2069
- );
2070
-
2071
- const chainIdHex = `0x${chainId.toString(16)}` as `0x${string}`;
2072
- const feeTokenCap = capabilities?.[chainIdHex]?.feeToken as FeeTokenCapability | undefined;
2073
-
2074
- if (!feeTokenCap?.supported || !feeTokenCap?.tokens?.length) {
2075
- if (isMounted) setFeeTokensLoading(false);
2076
- return;
2077
- }
2078
-
2079
- // Get RPC URL for balance fetching
2080
- const rpcUrl = viemChain?.rpcUrls?.default?.http?.[0] || `https://eth.llamarpc.com`;
2081
-
2082
- // Fetch balances in parallel
2083
- const tokensWithBalances = await Promise.all(
2084
- feeTokenCap.tokens.map(async (token) => {
2085
- try {
2086
- const balance = await fetchTokenBalance(token.address, request.data.address, rpcUrl);
2087
- const balanceFormatted = formatUnits(balance, token.decimals);
2088
- const tokenIsNative = isNativeToken(token.address);
2089
- // For native token (ETH): selectable if any balance (gas estimation will catch insufficient)
2090
- // For ERC-20 tokens: require at least 0.5 units
2091
- const isSelectable = tokenIsNative
2092
- ? balance > 0n
2093
- : parseFloat(balanceFormatted) >= 0.5;
2094
-
2095
- return {
2096
- uid: token.uid,
2097
- symbol: token.symbol,
2098
- address: token.address,
2099
- decimals: token.decimals,
2100
- balance,
2101
- balanceFormatted,
2102
- isNative: tokenIsNative,
2103
- isSelectable,
2104
- logoURI: token.logoURI,
2105
- } as FeeTokenOption;
2106
- } catch (error) {
2107
- console.warn(`Failed to fetch balance for ${token.symbol}:`, error);
2108
- return {
2109
- uid: token.uid,
2110
- symbol: token.symbol,
2111
- address: token.address,
2112
- decimals: token.decimals,
2113
- balance: 0n,
2114
- balanceFormatted: '0',
2115
- isNative: isNativeToken(token.address),
2116
- isSelectable: false,
2117
- logoURI: token.logoURI,
2118
- } as FeeTokenOption;
2119
- }
2120
- })
2121
- );
2122
-
2123
- if (isMounted) {
2124
- setFeeTokens(tokensWithBalances);
2125
- // Note: Initial token selection is handled by useGasEstimation hook
2126
- }
2127
- } catch (error) {
2128
- console.warn('[PermissionDialogWrapper] Failed to fetch fee tokens:', error);
2129
- } finally {
2130
- if (isMounted) setFeeTokensLoading(false);
2131
- }
2132
- };
2133
-
2134
- fetchFeeTokensData();
2135
-
2136
- return () => {
2137
- isMounted = false;
2138
- };
2139
- }, [chainId, apiKey, request.data.address, effectivePaymasterUrl, viemChain]);
2140
-
2141
- // Initialize account
2142
- // Note: Use effectivePaymasterUrl (stable) instead of computedPaymasterUrl to avoid
2143
- // re-initializing account when user changes fee token selection (which would cause
2144
- // gas estimation to run multiple times in a dependency cycle)
2145
- useEffect(() => {
2146
- let isMounted = true;
2147
-
2148
- const initializeAccount = async () => {
2149
- try {
2150
- const restoredAccount = await getAccountForSigning(
2151
- apiKey,
2152
- chainId,
2153
- effectivePaymasterUrl
2154
- );
2155
- if (isMounted) {
2156
- setAccount(restoredAccount);
2157
- }
2158
- } catch (error) {
2159
- console.error('[PermissionDialogWrapper] Error initializing account:', error);
2160
- }
2161
- };
2162
-
2163
- initializeAccount();
2164
-
2165
- return () => {
2166
- isMounted = false;
2167
- };
2168
- }, [apiKey, chainId, effectivePaymasterUrl]);
2169
-
2170
- // Note: Gas estimation is now handled by useGasEstimation hook
2171
-
2172
- // Convert to SpendPermission array format expected by PermissionDialog
2173
- const spends = useMemo(() => spendsData.map(spend => {
2174
- const tokenInfo = tokenInfoMap[spend.token] || (isNativeToken(spend.token)
2175
- ? { decimals: viemChain?.nativeCurrency?.decimals ?? 18, symbol: nativeSymbol }
2176
- : { decimals: 18, symbol: spend.token.slice(0, 6) + '...' + spend.token.slice(-4) });
2177
-
2178
- const allowance = BigInt(spend.allowance);
2179
- const amount = formatUnits(allowance, tokenInfo.decimals);
2180
- const limit = `${amount} ${tokenInfo.symbol}`;
2181
-
2182
- // Format duration with multiplier (defaults to 1 if not provided)
2183
- const multiplier = spend.multiplier ?? 1;
2184
- const duration = `${multiplier} ${spend.unit}${multiplier > 1 ? 's' : ''}`;
2185
-
2186
- return {
2187
- amount,
2188
- token: isNativeToken(spend.token) ? `Native (${nativeSymbol})` : tokenInfo.symbol,
2189
- tokenAddress: spend.token,
2190
- duration,
2191
- limit,
2192
- };
2193
- }), [spendsData, tokenInfoMap, viemChain, nativeSymbol]);
2194
-
2195
- // Format call permissions
2196
- const calls = useMemo(() => callsData.map(call => ({
2197
- target: call.target,
2198
- selector: call.selector,
2199
- functionSignature: call.functionSignature || (call.selector ? resolveFunctionSelector(call.selector) : 'Unknown Function'),
2200
- })), [callsData]);
2201
-
2202
- // Format expiry date
2203
- const expiryDate = useMemo(() => {
2204
- return formatExpiryDate(request.data.expiry);
2205
- }, [request.data.expiry]);
2206
-
2207
- // Generate warning message based on actual permissions
2208
- const warningMessage = useMemo(() => {
2209
- const parts: string[] = [];
2210
-
2211
- // Describe spend permissions
2212
- if (spends.length > 0) {
2213
- const spendDescriptions = spends.map(spend => {
2214
- // Remove "1 " prefix from duration (e.g., "1 Day" -> "day", "1 Week" -> "week")
2215
- const normalizedDuration = spend.duration.replace(/^1\s+/, '').toLowerCase();
2216
- // Handle "forever" specially - no "per" prefix needed
2217
- if (normalizedDuration === 'forever') {
2218
- return spend.limit;
2219
- }
2220
- return `${spend.limit} per ${normalizedDuration}`;
2221
- });
2222
- parts.push(`spend up to ${spendDescriptions.join(', ')}`);
2223
- }
2224
-
2225
- // Describe call permissions
2226
- if (calls.length > 0) {
2227
- const callDescriptions = calls.map(call => {
2228
- const fnName = call.functionSignature;
2229
- // Check for special selectors
2230
- if (fnName === 'Any Function') {
2231
- return 'call any function';
2232
- }
2233
- if (fnName === 'Empty Calldata') {
2234
- return 'send transactions with empty calldata';
2235
- }
2236
- // Extract just the function name from signature like "transfer(address,uint256)"
2237
- const simpleName = fnName.split('(')[0];
2238
- return `call ${simpleName}`;
2239
- });
2240
-
2241
- // Deduplicate and join
2242
- const uniqueCalls = [...new Set(callDescriptions)];
2243
- parts.push(uniqueCalls.join(', '));
2244
- }
2245
-
2246
- if (parts.length === 0) {
2247
- return `You are granting permissions to this dApp until ${expiryDate}. Only approve if you trust this dApp.`;
2248
- }
2249
-
2250
- return `This will allow the dApp to ${parts.join(' and ')} on your behalf until ${expiryDate}. Only approve if you trust this dApp.`;
2251
- }, [spends, calls, expiryDate]);
2252
-
2253
- const handleConfirm = async () => {
2254
- if (!account) {
2255
- console.error('[PermissionDialogWrapper] Account not initialized');
2256
- return;
2257
- }
2258
-
2259
- setIsProcessing(true);
2260
- setStatus('Granting permissions...');
2261
- try {
2262
- // Use the spends array directly from the request (already in correct format)
2263
- const permissionsDetail = {
2264
- spends: request.data.permissions.spends || [],
2265
- calls: request.data.permissions.calls,
2266
- };
2267
-
2268
- // Grant permissions using Account class with paymaster context
2269
- const result = await account.grantPermissions(
2270
- request.data.expiry,
2271
- request.data.spender as Address,
2272
- permissionsDetail,
2273
- computedPaymasterUrl,
2274
- computedPaymasterContext
2275
- );
2276
-
2277
- setStatus('Permissions granted successfully!');
2278
- onApprove(result);
2279
- } catch (error) {
2280
- console.error('Permission grant failed:', error);
2281
- const errorObj = error instanceof Error ? error : new Error(String(error));
2282
- setStatus(`Error: ${errorObj.message}`);
2283
- // Check if user cancelled passkey prompt (NotAllowedError)
2284
- if (errorObj.name === 'NotAllowedError') {
2285
- onReject(UIError.userRejected('User cancelled the passkey prompt'));
2286
- } else {
2287
- // Internal error
2288
- onReject(new UIError(standardErrorCodes.rpc.internal as UIErrorCode, errorObj.message));
2289
- }
2290
- } finally {
2291
- setIsProcessing(false);
2292
- }
2293
- };
2294
-
2295
- const handleCancel = () => {
2296
- setOpen(false);
2297
- onReject(UIError.userRejected());
2298
- };
2299
-
2300
- return (
2301
- <PermissionDialog
2302
- open={open}
2303
- onOpenChange={(newOpen) => {
2304
- if (!newOpen) handleCancel();
2305
- else setOpen(newOpen);
2306
- }}
2307
- mode="grant"
2308
- spenderAddress={request.data.spender}
2309
- origin={typeof window !== 'undefined' ? window.location.origin : 'unknown'}
2310
- spends={spends}
2311
- calls={calls}
2312
- expiryDate={expiryDate}
2313
- networkName={networkName}
2314
- chainId={chainId}
2315
- apiKey={apiKey}
2316
- onConfirm={handleConfirm}
2317
- onCancel={handleCancel}
2318
- isProcessing={isProcessing}
2319
- status={status}
2320
- isLoadingTokenInfo={isLoadingTokenInfo}
2321
- timestamp={new Date(request.timestamp)}
2322
- warningMessage={warningMessage}
2323
- gasFee={gasFee}
2324
- gasFeeLoading={gasFeeLoading}
2325
- gasEstimationError={gasEstimationError}
2326
- sponsored={isSponsored}
2327
- mainnetRpcUrl={getMainnetRpcUrl(apiKey)}
2328
- // Fee token props for ERC-20 paymaster
2329
- feeTokens={feeTokens}
2330
- feeTokensLoading={feeTokensLoading}
2331
- selectedFeeToken={selectedFeeToken}
2332
- onFeeTokenSelect={setSelectedFeeToken}
2333
- showFeeTokenSelector={!isSponsored && feeTokens.some(t => !t.isNative)}
2334
- isPayingWithErc20={isPayingWithErc20}
2335
- />
2336
- );
2337
- }
2338
-
2339
- // SiweDialogWrapper - handles Sign-In with Ethereum messages
2340
- function SiweDialogWrapper({
2341
- request,
2342
- onApprove,
2343
- onReject,
2344
- apiKey,
2345
- defaultChainId,
2346
- paymasters,
2347
- }: {
2348
- request: SignatureUIRequest;
2349
- onApprove: (data: any) => void;
2350
- onReject: (error?: Error) => void;
2351
- apiKey?: string;
2352
- defaultChainId?: number;
2353
- paymasters?: Record<number, PaymasterConfig>;
2354
- }) {
2355
- const [open, setOpen] = useState(true);
2356
- const [isProcessing, setIsProcessing] = useState(false);
2357
- const [siweStatus, setSiweStatus] = useState<string>('');
2358
-
2359
- // Use chainId from request (current chain), fallback to defaultChainId
2360
- const chainId = request.data.chainId || defaultChainId || 1;
2361
- const chainName = getChainNameFromId(chainId);
2362
- const chainIcon = useChainIconURI(chainId, apiKey, 24);
2363
- const origin = typeof window !== 'undefined' ? window.location.origin : 'unknown';
2364
-
2365
- // Decode message if it's hex encoded
2366
- const decodedMessage = useMemo(() => {
2367
- const msg = request.data.message;
2368
- if (msg.startsWith('0x')) {
2369
- try {
2370
- return hexToUtf8(msg);
2371
- } catch {
2372
- return msg;
2373
- }
2374
- }
2375
- return msg;
2376
- }, [request.data.message]);
2377
-
2378
- // Extract app name from SIWE message
2379
- const appName = useMemo(() => {
2380
- const match = decodedMessage.match(/^([^\n]+)\s+wants you to sign in/);
2381
- return match ? match[1] : 'dApp';
2382
- }, [decodedMessage]);
2383
-
2384
- // Generate warning if URI in SIWE message doesn't match current origin
2385
- const warningMessage = useMemo(() => {
2386
- try {
2387
- // Extract URI from SIWE message
2388
- const uriMatch = decodedMessage.match(/URI:\s*(.+)/);
2389
- if (!uriMatch) return undefined;
2390
-
2391
- const siweUri = uriMatch[1].trim();
2392
- const siweOrigin = new URL(siweUri).origin;
2393
- const currentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
2394
-
2395
- if (siweOrigin !== currentOrigin) {
2396
- return `The sign-in request is for "${siweUri}" but you are currently on "${currentOrigin}". This may be a phishing attempt.`;
2397
- }
2398
- } catch {
2399
- // If URI parsing fails, don't show warning
2400
- }
2401
- return undefined;
2402
- }, [decodedMessage]);
2403
-
2404
- const handleSign = async () => {
2405
- setIsProcessing(true);
2406
- setSiweStatus('Signing message...');
2407
- try {
2408
- // Restore account for signing
2409
- const account = await getAccountForSigning(
2410
- apiKey,
2411
- chainId,
2412
- paymasters?.[chainId]?.url
2413
- );
2414
-
2415
- // Sign the message
2416
- const signature = await account.signMessage(request.data.message);
2417
-
2418
- setSiweStatus('Sign-in successful!');
2419
- onApprove(signature);
2420
- } catch (error) {
2421
- console.error('SIWE signature failed:', error);
2422
- const errorObj = error instanceof Error ? error : new Error(String(error));
2423
- setSiweStatus(`Error: ${errorObj.message}`);
2424
- // Check if user cancelled passkey prompt (NotAllowedError)
2425
- if (errorObj.name === 'NotAllowedError') {
2426
- onReject(UIError.userRejected('User cancelled the passkey prompt'));
2427
- } else {
2428
- // Internal error
2429
- onReject(new UIError(standardErrorCodes.rpc.internal as UIErrorCode, errorObj.message));
2430
- }
2431
- } finally {
2432
- setIsProcessing(false);
2433
- }
2434
- };
2435
-
2436
- const handleCancel = () => {
2437
- setOpen(false);
2438
- onReject(UIError.userRejected());
2439
- };
2440
-
2441
- return (
2442
- <SiweDialog
2443
- open={open}
2444
- onOpenChange={(newOpen) => {
2445
- if (!newOpen) handleCancel();
2446
- else setOpen(newOpen);
2447
- }}
2448
- message={decodedMessage}
2449
- origin={origin}
2450
- timestamp={new Date(request.timestamp)}
2451
- appName={appName}
2452
- accountAddress={request.data.address}
2453
- chainName={chainName}
2454
- chainId={chainId}
2455
- chainIcon={chainIcon}
2456
- mainnetRpcUrl={getMainnetRpcUrl(apiKey)}
2457
- onSign={handleSign}
2458
- onCancel={handleCancel}
2459
- isProcessing={isProcessing}
2460
- siweStatus={siweStatus}
2461
- canSign={!isProcessing}
2462
- warningMessage={warningMessage}
2463
- />
2464
- );
2465
- }
2466
-
2467
- // RevokePermissionDialogWrapper - handles wallet_revokePermissions
2468
- function RevokePermissionDialogWrapper({
2469
- request,
2470
- onApprove,
2471
- onReject,
2472
- apiKey,
2473
- defaultChainId,
2474
- paymasters,
2475
- }: {
2476
- request: RevokePermissionUIRequest;
2477
- onApprove: (data: any) => void;
2478
- onReject: (error?: Error) => void;
2479
- apiKey?: string;
2480
- defaultChainId?: number;
2481
- paymasters?: Record<number, PaymasterConfig>;
2482
- }) {
2483
- const [open, setOpen] = useState(true);
2484
- const [isProcessing, setIsProcessing] = useState(false);
2485
- const [status, setStatus] = useState<string>('');
2486
- const [isLoadingPermissionDetails, setIsLoadingPermissionDetails] = useState(true);
2487
- const [fetchedPermissionData, setFetchedPermissionData] = useState<any>(null);
2488
- const [tokenInfoMap, setTokenInfoMap] = useState<TokenInfoMap>({});
2489
- const [account, setAccount] = useState<Account | null>(null);
2490
- const [feeTokens, setFeeTokens] = useState<FeeTokenOption[]>([]);
2491
- const [feeTokensLoading, setFeeTokensLoading] = useState(true);
2492
-
2493
- const chainId = request.data.chainId || defaultChainId || 1;
2494
- const viemChain = SUPPORTED_CHAINS.find(c => c.id === chainId);
2495
- const networkName = viemChain?.name || 'Unknown Network';
2496
-
2497
- // Get native token symbol from feeTokens (defaults to ETH if not found)
2498
- const nativeToken = feeTokens?.find(t => t.isNative);
2499
- const nativeSymbol = nativeToken?.symbol || viemChain?.nativeCurrency?.symbol || 'ETH';
2500
-
2501
- // Fetch native token price dynamically based on the chain's native token symbol
2502
- const nativeTokenPrice = useFeeTokenPrice(nativeSymbol);
2503
-
2504
- // Extract paymasterUrl from capabilities (EIP-5792 paymasterService capability)
2505
- // Priority: capabilities.paymasterService.url > paymasters[chainId].url
2506
- const effectivePaymasterUrl = useMemo(() => {
2507
- const capabilitiesPaymasterUrl = request.data.capabilities?.paymasterService?.url;
2508
- return capabilitiesPaymasterUrl || paymasters?.[chainId]?.url;
2509
- }, [request.data.capabilities?.paymasterService?.url, paymasters, chainId]);
2510
-
2511
- // Extract paymasterContext from capabilities (EIP-5792 paymasterService capability)
2512
- // Priority: capabilities.paymasterService.context > paymasters[chainId].context
2513
- const effectivePaymasterContext = useMemo(() => {
2514
- const capabilitiesPaymasterContext = (request.data.capabilities?.paymasterService as { context?: Record<string, unknown> } | undefined)?.context;
2515
- return capabilitiesPaymasterContext || paymasters?.[chainId]?.context;
2516
- }, [request.data.capabilities?.paymasterService, paymasters, chainId]);
2517
-
2518
- // Check if this is a sponsored transaction (paymaster provided)
2519
- const isSponsored = !!effectivePaymasterUrl;
2520
-
2521
- // Build the actual permission revoke call for gas estimation
2522
- const transactionCalls = useMemo(() => {
2523
- if (!fetchedPermissionData) return [];
2524
-
2525
- try {
2526
- const revokeCall = buildRevokePermissionCall(fetchedPermissionData);
2527
- return [revokeCall];
2528
- } catch (error) {
2529
- console.warn('[RevokePermissionDialogWrapper] Failed to build permission revoke call:', error);
2530
- return [];
2531
- }
2532
- }, [fetchedPermissionData]);
2533
-
2534
- // Use the gas estimation hook for both ETH and ERC-20 cost estimation
2535
- const {
2536
- gasFee,
2537
- gasFeeLoading,
2538
- gasEstimationError,
2539
- tokenEstimates,
2540
- selectedFeeToken,
2541
- setSelectedFeeToken,
2542
- isPayingWithErc20,
2543
- } = useGasEstimation({
2544
- account,
2545
- transactionCalls,
2546
- chainId,
2547
- apiKey,
2548
- feeTokens,
2549
- isSponsored,
2550
- onFeeTokensUpdate: setFeeTokens,
2551
- });
2552
-
2553
- // Compute paymaster URL based on fee token selection (for ERC-20 paymaster)
2554
- const computedPaymasterUrl = useMemo(() => {
2555
- // If already sponsored via capabilities or config, use that
2556
- if (effectivePaymasterUrl) return effectivePaymasterUrl;
2557
-
2558
- // If user selected an ERC-20 token (non-native), use ERC-20 paymaster
2559
- if (selectedFeeToken && !selectedFeeToken.isNative) {
2560
- return `${JAW_PAYMASTER_URL}?chainId=${chainId}${apiKey ? `&api-key=${apiKey}` : ''}`;
2561
- }
2562
-
2563
- // Native ETH - no paymaster needed
2564
- return undefined;
2565
- }, [effectivePaymasterUrl, selectedFeeToken, chainId, apiKey]);
2566
-
2567
- // Compute paymaster context based on fee token selection
2568
- const computedPaymasterContext = useMemo(() => {
2569
- // If using ERC-20 paymaster, include token address and gas amount in context
2570
- if (selectedFeeToken && !selectedFeeToken.isNative) {
2571
- // Use the actual estimate from tokenEstimates if available
2572
- const estimate = tokenEstimates.find(
2573
- e => e.tokenAddress.toLowerCase() === selectedFeeToken.address.toLowerCase()
2574
- );
2575
-
2576
- if (estimate) {
2577
- // Use the actual token cost from paymaster quote
2578
- return {
2579
- token: selectedFeeToken.address,
2580
- gas: estimate.tokenCost.toString(),
2581
- };
2582
- }
2583
-
2584
- // Fallback to client-side calculation if no estimate yet
2585
- const gasUsd = gasFee && nativeTokenPrice ? nativeTokenPrice * Number(gasFee) : 0;
2586
- const gasInTokenUnits = Math.ceil(gasUsd * Math.pow(10, selectedFeeToken.decimals));
2587
- return {
2588
- token: selectedFeeToken.address,
2589
- gas: gasInTokenUnits.toString(),
2590
- };
2591
- }
2592
- return effectivePaymasterContext;
2593
- }, [selectedFeeToken, effectivePaymasterContext, tokenEstimates, gasFee, nativeTokenPrice]);
2594
-
2595
- const chain = useMemo(
2596
- () => buildChainConfigFromApiKey(chainId, apiKey, computedPaymasterUrl),
2597
- [chainId, apiKey, computedPaymasterUrl]
2598
- );
2599
-
2600
- // Fetch permission details from relay
2601
- useEffect(() => {
2602
- if (!request.data.permissionId || !apiKey) {
2603
- setIsLoadingPermissionDetails(false);
2604
- return;
2605
- }
2606
-
2607
- const fetchPermissionDetails = async () => {
2608
- try {
2609
- const permData = await getPermissionFromRelay(request.data.permissionId as `0x${string}`, apiKey);
2610
- console.log('✅ Fetched permission details from relay:', permData);
2611
- setFetchedPermissionData(permData);
2612
-
2613
- // Fetch token info for spends
2614
- if (permData.spends && permData.spends.length > 0) {
2615
- const newTokenInfoMap: TokenInfoMap = {};
2616
- for (const spend of permData.spends) {
2617
- const tokenAddress = spend.token;
2618
- if (isNativeToken(tokenAddress)) {
2619
- newTokenInfoMap[tokenAddress] = {
2620
- decimals: viemChain?.nativeCurrency?.decimals ?? 18,
2621
- symbol: viemChain?.nativeCurrency?.symbol || 'ETH'
2622
- };
2623
- } else {
2624
- try {
2625
- const publicClient = createPublicClient({
2626
- chain: {
2627
- id: chainId,
2628
- name: networkName,
2629
- nativeCurrency: viemChain?.nativeCurrency || { name: 'Ether', symbol: 'ETH', decimals: 18 },
2630
- rpcUrls: {
2631
- default: { http: [chain.rpcUrl || ''] },
2632
- public: { http: [chain.rpcUrl || ''] },
2633
- },
2634
- },
2635
- transport: http(chain.rpcUrl),
2636
- });
2637
-
2638
- const [decimals, symbol] = await Promise.all([
2639
- publicClient.readContract({
2640
- address: tokenAddress as Address,
2641
- abi: erc20Abi,
2642
- functionName: 'decimals',
2643
- }),
2644
- publicClient.readContract({
2645
- address: tokenAddress as Address,
2646
- abi: erc20Abi,
2647
- functionName: 'symbol',
2648
- }),
2649
- ]);
2650
- newTokenInfoMap[tokenAddress] = { decimals, symbol };
2651
- } catch {
2652
- newTokenInfoMap[tokenAddress] = { decimals: 18, symbol: tokenAddress };
2653
- }
2654
- }
2655
- }
2656
- setTokenInfoMap(newTokenInfoMap);
2657
- }
2658
- setIsLoadingPermissionDetails(false);
2659
- } catch (error) {
2660
- console.error('❌ Failed to fetch permission details:', error);
2661
- setIsLoadingPermissionDetails(false);
2662
- }
2663
- };
2664
-
2665
- fetchPermissionDetails();
2666
- }, [request.data.permissionId, apiKey, chainId, networkName, chain.rpcUrl, viemChain]);
2667
-
2668
- // Fetch fee tokens for ERC-20 paymaster support
2669
- useEffect(() => {
2670
- // Skip if already sponsored via capabilities/config
2671
- if (effectivePaymasterUrl) {
2672
- setFeeTokensLoading(false);
2673
- return;
2674
- }
2675
-
2676
- let isMounted = true;
2677
-
2678
- const fetchFeeTokensData = async () => {
2679
- try {
2680
- // Fetch capabilities to get available fee tokens
2681
- const capabilities = await handleGetCapabilitiesRequest(
2682
- { method: 'wallet_getCapabilities', params: [] },
2683
- apiKey || '',
2684
- true // showTestnets
2685
- );
2686
-
2687
- const chainIdHex = `0x${chainId.toString(16)}` as `0x${string}`;
2688
- const feeTokenCap = capabilities?.[chainIdHex]?.feeToken as FeeTokenCapability | undefined;
2689
-
2690
- if (!feeTokenCap?.supported || !feeTokenCap?.tokens?.length) {
2691
- if (isMounted) setFeeTokensLoading(false);
2692
- return;
2693
- }
2694
-
2695
- // Get RPC URL for balance fetching
2696
- const rpcUrl = viemChain?.rpcUrls?.default?.http?.[0] || `https://eth.llamarpc.com`;
2697
-
2698
- // Fetch balances in parallel
2699
- const tokensWithBalances = await Promise.all(
2700
- feeTokenCap.tokens.map(async (token) => {
2701
- try {
2702
- const balance = await fetchTokenBalance(token.address, request.data.address as Address, rpcUrl);
2703
- const balanceFormatted = formatUnits(balance, token.decimals);
2704
- const tokenIsNative = isNativeToken(token.address);
2705
- // For native token (ETH): selectable if any balance (gas estimation will catch insufficient)
2706
- // For ERC-20 tokens: require at least 0.5 units
2707
- const isSelectable = tokenIsNative
2708
- ? balance > 0n
2709
- : parseFloat(balanceFormatted) >= 0.5;
2710
-
2711
- return {
2712
- uid: token.uid,
2713
- symbol: token.symbol,
2714
- address: token.address,
2715
- decimals: token.decimals,
2716
- balance,
2717
- balanceFormatted,
2718
- isNative: tokenIsNative,
2719
- isSelectable,
2720
- logoURI: token.logoURI,
2721
- } as FeeTokenOption;
2722
- } catch (error) {
2723
- console.warn(`Failed to fetch balance for ${token.symbol}:`, error);
2724
- return {
2725
- uid: token.uid,
2726
- symbol: token.symbol,
2727
- address: token.address,
2728
- decimals: token.decimals,
2729
- balance: 0n,
2730
- balanceFormatted: '0',
2731
- isNative: isNativeToken(token.address),
2732
- isSelectable: false,
2733
- logoURI: token.logoURI,
2734
- } as FeeTokenOption;
2735
- }
2736
- })
2737
- );
2738
-
2739
- if (isMounted) {
2740
- setFeeTokens(tokensWithBalances);
2741
- // Note: Initial token selection is handled by useGasEstimation hook
2742
- }
2743
- } catch (error) {
2744
- console.warn('[RevokePermissionDialogWrapper] Failed to fetch fee tokens:', error);
2745
- } finally {
2746
- if (isMounted) setFeeTokensLoading(false);
2747
- }
2748
- };
2749
-
2750
- fetchFeeTokensData();
2751
-
2752
- return () => {
2753
- isMounted = false;
2754
- };
2755
- }, [chainId, apiKey, request.data.address, effectivePaymasterUrl, viemChain]);
2756
-
2757
- // Initialize account
2758
- // Note: Use effectivePaymasterUrl (stable) instead of computedPaymasterUrl to avoid
2759
- // re-initializing account when user changes fee token selection (which would cause
2760
- // gas estimation to run multiple times in a dependency cycle)
2761
- useEffect(() => {
2762
- let isMounted = true;
2763
-
2764
- const initializeAccount = async () => {
2765
- try {
2766
- const restoredAccount = await getAccountForSigning(
2767
- apiKey,
2768
- chainId,
2769
- effectivePaymasterUrl
2770
- );
2771
- if (isMounted) {
2772
- setAccount(restoredAccount);
2773
- }
2774
- } catch (error) {
2775
- console.error('[RevokePermissionDialogWrapper] Error initializing account:', error);
2776
- }
2777
- };
2778
-
2779
- initializeAccount();
2780
-
2781
- return () => {
2782
- isMounted = false;
2783
- };
2784
- }, [apiKey, chainId, effectivePaymasterUrl]);
2785
-
2786
- // Format spends for display
2787
- const formattedSpends = useMemo(() => {
2788
- if (!fetchedPermissionData?.spends) return [];
2789
-
2790
- return fetchedPermissionData.spends.map((spend: any) => {
2791
- const tokenAddress = spend.token;
2792
- const tokenInfo = tokenInfoMap[tokenAddress] || {
2793
- decimals: viemChain?.nativeCurrency?.decimals ?? 18,
2794
- symbol: nativeSymbol
2795
- };
2796
- const allowance = BigInt(spend.allowance);
2797
- const amount = formatUnits(allowance, tokenInfo.decimals);
2798
- const limit = `${amount} ${tokenInfo.symbol}`;
2799
- // Format duration with multiplier (unit is period string like 'day', 'week', etc.)
2800
- const multiplier = spend.multiplier ?? 1;
2801
- const duration = `${multiplier} ${spend.unit}${multiplier > 1 ? 's' : ''}`;
2802
-
2803
- return {
2804
- amount,
2805
- token: isNativeToken(tokenAddress)
2806
- ? `Native (${nativeSymbol})`
2807
- : tokenInfo.symbol,
2808
- tokenAddress,
2809
- duration,
2810
- limit,
2811
- };
2812
- });
2813
- }, [fetchedPermissionData, tokenInfoMap, viemChain, nativeSymbol]);
2814
-
2815
- // Format call permissions from fetched data
2816
- const formattedCalls = useMemo(() => {
2817
- if (!fetchedPermissionData?.calls) return [];
2818
-
2819
- return fetchedPermissionData.calls.map((call: any) => ({
2820
- target: call.target,
2821
- selector: call.selector,
2822
- functionSignature: call.functionSignature || (call.selector ? resolveFunctionSelector(call.selector) : 'Unknown Function'),
2823
- }));
2824
- }, [fetchedPermissionData]);
2825
-
2826
- // Expiry date from fetched permission
2827
- const expiryDate = useMemo(() => {
2828
- if (!fetchedPermissionData) return '';
2829
- const endTimestamp = parseInt(fetchedPermissionData.end, 10);
2830
- return formatExpiryDate(endTimestamp);
2831
- }, [fetchedPermissionData]);
2832
-
2833
- // Spender address from fetched permission
2834
- const spenderAddress = fetchedPermissionData?.spender || '0x...';
2835
-
2836
- const handleConfirm = async () => {
2837
- if (!account) {
2838
- console.error('[RevokePermissionDialogWrapper] Account not initialized');
2839
- return;
2840
- }
2841
-
2842
- setIsProcessing(true);
2843
- setStatus('Revoking permission...');
2844
- try {
2845
- // Revoke permission using Account class with paymaster context
2846
- await account.revokePermission(
2847
- request.data.permissionId as `0x${string}`,
2848
- computedPaymasterUrl,
2849
- computedPaymasterContext
2850
- );
2851
-
2852
- console.log('Permission revoked');
2853
- setStatus('Permission revoked successfully!');
2854
- onApprove({ success: true });
2855
- } catch (error) {
2856
- console.error('Permission revoke failed:', error);
2857
- const errorObj = error instanceof Error ? error : new Error(String(error));
2858
- setStatus(`Error: ${errorObj.message}`);
2859
- // Check if user cancelled passkey prompt (NotAllowedError)
2860
- if (errorObj.name === 'NotAllowedError') {
2861
- onReject(UIError.userRejected('User cancelled the passkey prompt'));
2862
- } else {
2863
- // Internal error
2864
- onReject(new UIError(standardErrorCodes.rpc.internal as UIErrorCode, errorObj.message));
2865
- }
2866
- } finally {
2867
- setIsProcessing(false);
2868
- }
2869
- };
2870
-
2871
- const handleCancel = () => {
2872
- setOpen(false);
2873
- onReject(UIError.userRejected());
2874
- };
2875
-
2876
- return (
2877
- <PermissionDialog
2878
- open={open}
2879
- onOpenChange={(newOpen) => {
2880
- if (!newOpen) handleCancel();
2881
- else setOpen(newOpen);
2882
- }}
2883
- mode="revoke"
2884
- permissionId={request.data.permissionId}
2885
- spenderAddress={spenderAddress}
2886
- origin={typeof window !== 'undefined' ? window.location.origin : 'unknown'}
2887
- spends={formattedSpends}
2888
- calls={formattedCalls}
2889
- expiryDate={expiryDate}
2890
- networkName={networkName}
2891
- chainId={chainId}
2892
- apiKey={apiKey}
2893
- onConfirm={handleConfirm}
2894
- onCancel={handleCancel}
2895
- isProcessing={isProcessing}
2896
- status={status}
2897
- isLoadingTokenInfo={isLoadingPermissionDetails}
2898
- timestamp={new Date(request.timestamp)}
2899
- mainnetRpcUrl={getMainnetRpcUrl(apiKey)}
2900
- // Gas estimation props
2901
- gasFee={gasFee}
2902
- gasFeeLoading={gasFeeLoading}
2903
- gasEstimationError={gasEstimationError}
2904
- sponsored={isSponsored}
2905
- // Fee token props for ERC-20 paymaster
2906
- feeTokens={feeTokens}
2907
- feeTokensLoading={feeTokensLoading}
2908
- selectedFeeToken={selectedFeeToken}
2909
- onFeeTokenSelect={setSelectedFeeToken}
2910
- showFeeTokenSelector={!isSponsored && feeTokens.some(t => !t.isNative)}
2911
- isPayingWithErc20={isPayingWithErc20}
2912
- />
2913
- );
2914
- }
2915
-
2916
- // UnsupportedMethodDialogWrapper - handles unknown/unsupported methods
2917
- function UnsupportedMethodDialogWrapper({
2918
- method,
2919
- onReject,
2920
- }: {
2921
- method: string;
2922
- onReject: (error?: Error) => void;
2923
- }) {
2924
- const [open, setOpen] = useState(true);
2925
- const [isClosing, setIsClosing] = useState(false);
2926
-
2927
- const origin = typeof window !== 'undefined' ? window.location.origin : 'unknown';
2928
-
2929
- const handleClose = () => {
2930
- if (!isClosing) {
2931
- setIsClosing(true);
2932
- console.log('❌ Unsupported method:', method);
2933
- setOpen(false);
2934
- // Use UIError.unsupportedRequest which has proper error code
2935
- onReject(UIError.unsupportedRequest(method));
2936
- }
2937
- };
2938
-
2939
- return (
2940
- <DefaultDialogComponent
2941
- open={open}
2942
- onOpenChange={(newOpen) => {
2943
- if (!newOpen) handleClose();
2944
- else setOpen(newOpen);
2945
- }}
2946
- handleClose={handleClose}
2947
- contentStyle={{
2948
- width: 'fit-content',
2949
- maxWidth: '450px',
2950
- }}
2951
- >
2952
- <div className="flex flex-col gap-6 p-6">
2953
- {/* Error Icon */}
2954
- <div className="flex justify-center">
2955
- <div className="w-16 h-16 bg-orange-100 rounded-full flex items-center justify-center">
2956
- <svg
2957
- className="w-8 h-8 text-orange-600"
2958
- fill="none"
2959
- stroke="currentColor"
2960
- viewBox="0 0 24 24"
2961
- >
2962
- <path
2963
- strokeLinecap="round"
2964
- strokeLinejoin="round"
2965
- strokeWidth={2}
2966
- d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
2967
- />
2968
- </svg>
2969
- </div>
2970
- </div>
2971
-
2972
- {/* Content */}
2973
- <div className="text-center">
2974
- <h3 className="text-xl font-bold text-gray-900 mb-2">
2975
- Unsupported Method
2976
- </h3>
2977
- <p className="text-sm text-gray-600 mb-4">
2978
- This wallet does not support the following method:
2979
- </p>
2980
- <div className="bg-gray-100 rounded-lg p-4">
2981
- <code className="text-sm font-mono text-gray-900 break-all">
2982
- {method}
2983
- </code>
2984
- </div>
2985
- </div>
2986
-
2987
- {/* Origin */}
2988
- <p className="text-xs text-gray-500 text-center">
2989
- Origin: {origin}
2990
- </p>
2991
-
2992
- {/* Close Button */}
2993
- <button
2994
- onClick={handleClose}
2995
- disabled={isClosing}
2996
- className="w-full py-3 px-6 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-semibold rounded-lg transition-colors"
2997
- >
2998
- {isClosing ? 'Closing...' : 'Close'}
2999
- </button>
3000
- </div>
3001
- </DefaultDialogComponent>
3002
- );
3003
- }
3004
-