@jaw.id/ui 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs-error.cjs +3 -0
- package/dist/{ccip-BYa9WTP9.js → ccip-yRQNp5zr.js} +1 -1
- package/dist/{index-DBYNWeek.js → index-BcDVfcdq.js} +20832 -20511
- package/dist/index.js +1 -1
- package/package.json +8 -2
- package/.babelrc +0 -12
- package/CHANGELOG.md +0 -23
- package/components.json +0 -22
- package/postcss.config.cjs +0 -6
- package/src/components/ConnectDialog/index.tsx +0 -270
- package/src/components/ConnectDialog/types.ts +0 -34
- package/src/components/DefaultDialog/index.tsx +0 -99
- package/src/components/Eip712Dialog/index.tsx +0 -525
- package/src/components/Eip712Dialog/types.ts +0 -27
- package/src/components/FeeTokenSelector/index.tsx +0 -308
- package/src/components/OnboardingDialog/index.tsx +0 -317
- package/src/components/OnboardingDialog/types.ts +0 -43
- package/src/components/OrSeparator/index.tsx +0 -13
- package/src/components/PermissionDialog/index.tsx +0 -598
- package/src/components/PermissionDialog/types.ts +0 -77
- package/src/components/SignatureDialog/index.tsx +0 -180
- package/src/components/SignatureDialog/types.ts +0 -27
- package/src/components/SiweDialog/index.tsx +0 -231
- package/src/components/SiweDialog/types.ts +0 -34
- package/src/components/TransactionDialog/DecodedCalldata.tsx +0 -79
- package/src/components/TransactionDialog/index.tsx +0 -663
- package/src/components/TransactionDialog/types.ts +0 -54
- package/src/components/ui/accordion.tsx +0 -66
- package/src/components/ui/avatar.tsx +0 -53
- package/src/components/ui/button.tsx +0 -59
- package/src/components/ui/card.tsx +0 -92
- package/src/components/ui/checkbox.tsx +0 -32
- package/src/components/ui/dialog.tsx +0 -183
- package/src/components/ui/dropdown-menu.tsx +0 -258
- package/src/components/ui/form.tsx +0 -167
- package/src/components/ui/input.tsx +0 -60
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/popover.tsx +0 -48
- package/src/components/ui/scroll-area.tsx +0 -58
- package/src/components/ui/select.tsx +0 -160
- package/src/components/ui/separator.tsx +0 -28
- package/src/components/ui/sheet.tsx +0 -169
- package/src/components/ui/spinner.tsx +0 -18
- package/src/components/ui/tabs.tsx +0 -69
- package/src/components/ui/tooltip.tsx +0 -61
- package/src/hooks/index.ts +0 -5
- package/src/hooks/useChainIconURI.tsx +0 -117
- package/src/hooks/useDecodedCalldata.ts +0 -128
- package/src/hooks/useFeeTokenPrice.tsx +0 -36
- package/src/hooks/useGasEstimation.ts +0 -474
- package/src/hooks/useIsMobile.ts +0 -36
- package/src/icons/index.tsx +0 -114
- package/src/index.ts +0 -19
- package/src/lib/utils.ts +0 -6
- package/src/react/ReactUIHandler.tsx +0 -3004
- package/src/react/index.ts +0 -2
- package/src/styles.css +0 -210
- package/src/utils/formatAddress.ts +0 -24
- package/src/utils/index.ts +0 -4
- package/src/utils/justaNameInstance.ts +0 -25
- package/src/utils/tokenBalance.ts +0 -41
- package/src/utils/tokenPrice.ts +0 -58
- package/tailwind.config.js +0 -130
- package/tsconfig.json +0 -19
- package/tsconfig.lib.json +0 -43
- 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
|
-
|