@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,128 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
2
|
-
import { createPublicClient, http, decodeFunctionData, type Abi, type Hex } from 'viem';
|
|
3
|
-
import { whatsabi } from '@shazow/whatsabi';
|
|
4
|
-
import { JAW_RPC_URL } from '@jaw.id/core';
|
|
5
|
-
|
|
6
|
-
export interface DecodedParam {
|
|
7
|
-
name: string;
|
|
8
|
-
type: string;
|
|
9
|
-
value: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface DecodedCalldata {
|
|
13
|
-
functionName: string;
|
|
14
|
-
signature: string;
|
|
15
|
-
params: DecodedParam[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Module-level ABI cache keyed by lowercase address
|
|
19
|
-
const abiCache = new Map<string, Abi>();
|
|
20
|
-
|
|
21
|
-
// Deduplicates concurrent fetches for the same contract
|
|
22
|
-
const inflightRequests = new Map<string, Promise<Abi>>();
|
|
23
|
-
|
|
24
|
-
async function fetchAbi(address: string, rpcUrl: string): Promise<Abi> {
|
|
25
|
-
const key = address.toLowerCase();
|
|
26
|
-
|
|
27
|
-
const cached = abiCache.get(key);
|
|
28
|
-
if (cached) return cached;
|
|
29
|
-
|
|
30
|
-
const inflight = inflightRequests.get(key);
|
|
31
|
-
if (inflight) return inflight;
|
|
32
|
-
|
|
33
|
-
const promise = (async () => {
|
|
34
|
-
const client = createPublicClient({ transport: http(rpcUrl) });
|
|
35
|
-
const result = await whatsabi.autoload(address, {
|
|
36
|
-
provider: client,
|
|
37
|
-
followProxies: true,
|
|
38
|
-
abiLoader: false,
|
|
39
|
-
signatureLookup: new whatsabi.loaders.OpenChainSignatureLookup(),
|
|
40
|
-
});
|
|
41
|
-
const abi = result.abi as Abi;
|
|
42
|
-
abiCache.set(key, abi);
|
|
43
|
-
return abi;
|
|
44
|
-
})();
|
|
45
|
-
|
|
46
|
-
inflightRequests.set(key, promise);
|
|
47
|
-
try {
|
|
48
|
-
return await promise;
|
|
49
|
-
} finally {
|
|
50
|
-
inflightRequests.delete(key);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function formatParamValue(value: unknown): string {
|
|
55
|
-
if (typeof value === 'bigint') return value.toString();
|
|
56
|
-
if (Array.isArray(value)) return `[${value.map(formatParamValue).join(', ')}]`;
|
|
57
|
-
if (typeof value === 'object' && value !== null) return JSON.stringify(value);
|
|
58
|
-
return String(value);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function useDecodedCalldata(
|
|
62
|
-
to: string | undefined,
|
|
63
|
-
data: string | undefined,
|
|
64
|
-
chainId: number,
|
|
65
|
-
apiKey?: string
|
|
66
|
-
): { decoded: DecodedCalldata | null; isLoading: boolean } {
|
|
67
|
-
const [decoded, setDecoded] = useState<DecodedCalldata | null>(null);
|
|
68
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
69
|
-
|
|
70
|
-
// Early return conditions checked inside useEffect to keep hook call order stable
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
if (!to || !data || data === '0x' || data.length < 10) {
|
|
73
|
-
setDecoded(null);
|
|
74
|
-
setIsLoading(false);
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
let isMounted = true;
|
|
79
|
-
|
|
80
|
-
const decode = async () => {
|
|
81
|
-
setIsLoading(true);
|
|
82
|
-
try {
|
|
83
|
-
const rpcUrl = apiKey
|
|
84
|
-
? `${JAW_RPC_URL}?chainId=${chainId}&api-key=${apiKey}`
|
|
85
|
-
: `${JAW_RPC_URL}?chainId=${chainId}`;
|
|
86
|
-
|
|
87
|
-
const abi = await fetchAbi(to, rpcUrl);
|
|
88
|
-
if (!isMounted) return;
|
|
89
|
-
|
|
90
|
-
const { functionName, args } = decodeFunctionData({ abi, data: data as Hex });
|
|
91
|
-
|
|
92
|
-
// Find the matching ABI entry to get parameter names/types
|
|
93
|
-
const abiItem = abi.find(
|
|
94
|
-
(item) => 'name' in item && item.name === functionName && item.type === 'function'
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
const inputs = abiItem && 'inputs' in abiItem ? abiItem.inputs ?? [] : [];
|
|
98
|
-
|
|
99
|
-
const params: DecodedParam[] = (args ?? []).map((arg, i) => ({
|
|
100
|
-
name: inputs[i]?.name || `param${i}`,
|
|
101
|
-
type: inputs[i]?.type || 'unknown',
|
|
102
|
-
value: formatParamValue(arg),
|
|
103
|
-
}));
|
|
104
|
-
|
|
105
|
-
const signature = `${functionName}(${inputs.map((inp) => inp.type).join(', ')})`;
|
|
106
|
-
|
|
107
|
-
if (isMounted) {
|
|
108
|
-
setDecoded({ functionName, signature, params });
|
|
109
|
-
setIsLoading(false);
|
|
110
|
-
}
|
|
111
|
-
} catch (err) {
|
|
112
|
-
console.debug('[useDecodedCalldata] Failed to decode:', err);
|
|
113
|
-
if (isMounted) {
|
|
114
|
-
setDecoded(null);
|
|
115
|
-
setIsLoading(false);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
decode();
|
|
121
|
-
|
|
122
|
-
return () => {
|
|
123
|
-
isMounted = false;
|
|
124
|
-
};
|
|
125
|
-
}, [to, data, chainId, apiKey]);
|
|
126
|
-
|
|
127
|
-
return { decoded, isLoading };
|
|
128
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
2
|
-
import { fetchTokenPrice } from '../utils/tokenPrice';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Hook to fetch and cache the current price in USD for a token by its symbol
|
|
6
|
-
* Uses CryptoCompare API with a 5-minute cache
|
|
7
|
-
* @param symbol - The token symbol (ETH, AVAX, BNB, USDC, etc.)
|
|
8
|
-
* @returns The current token price in USD, or 0 if not yet fetched/failed
|
|
9
|
-
*/
|
|
10
|
-
export function useFeeTokenPrice(symbol?: string): number {
|
|
11
|
-
const [price, setPrice] = useState<number>(0);
|
|
12
|
-
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
if (!symbol) {
|
|
15
|
-
setPrice(0);
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
let isMounted = true;
|
|
20
|
-
|
|
21
|
-
const getPrice = async () => {
|
|
22
|
-
const fetchedPrice = await fetchTokenPrice(symbol);
|
|
23
|
-
if (isMounted) {
|
|
24
|
-
setPrice(fetchedPrice);
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
getPrice();
|
|
29
|
-
|
|
30
|
-
return () => {
|
|
31
|
-
isMounted = false;
|
|
32
|
-
};
|
|
33
|
-
}, [symbol]);
|
|
34
|
-
|
|
35
|
-
return price;
|
|
36
|
-
}
|
|
@@ -1,474 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
2
|
-
import type { Address, Hex } from 'viem';
|
|
3
|
-
import type { Account, TokenEstimate, TransactionCall } from '@jaw.id/core';
|
|
4
|
-
import { estimateErc20PaymasterCosts, JAW_PAYMASTER_URL } from '@jaw.id/core';
|
|
5
|
-
import type { FeeTokenOption } from '../components/FeeTokenSelector';
|
|
6
|
-
|
|
7
|
-
// ============================================================================
|
|
8
|
-
// Types
|
|
9
|
-
// ============================================================================
|
|
10
|
-
|
|
11
|
-
// Re-export TransactionCall from core for consumers
|
|
12
|
-
export type { TransactionCall } from '@jaw.id/core';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Configuration options for gas estimation
|
|
16
|
-
*/
|
|
17
|
-
export interface UseGasEstimationConfig {
|
|
18
|
-
/** The Account instance to estimate gas for */
|
|
19
|
-
account: Account | null;
|
|
20
|
-
/** Array of transaction calls to estimate */
|
|
21
|
-
transactionCalls: TransactionCall[];
|
|
22
|
-
/** Chain ID for the estimation */
|
|
23
|
-
chainId: number;
|
|
24
|
-
/** API key for paymaster access */
|
|
25
|
-
apiKey?: string;
|
|
26
|
-
/** Available fee tokens (ETH + ERC-20) */
|
|
27
|
-
feeTokens: FeeTokenOption[];
|
|
28
|
-
/** Whether the transaction is sponsored (gas covered by paymaster) */
|
|
29
|
-
isSponsored?: boolean;
|
|
30
|
-
/** Optional permission ID for permission-based execution */
|
|
31
|
-
permissionId?: Hex;
|
|
32
|
-
/** Callback when fee tokens are updated with estimates */
|
|
33
|
-
onFeeTokensUpdate?: (tokens: FeeTokenOption[]) => void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Result returned by the useGasEstimation hook
|
|
38
|
-
*/
|
|
39
|
-
export interface UseGasEstimationResult {
|
|
40
|
-
/** Estimated gas fee in ETH (as string) or 'sponsored' */
|
|
41
|
-
gasFee: string;
|
|
42
|
-
/** Whether gas estimation is in progress */
|
|
43
|
-
gasFeeLoading: boolean;
|
|
44
|
-
/** Error message if estimation failed */
|
|
45
|
-
gasEstimationError: string;
|
|
46
|
-
/** Token cost estimates for ERC-20 payment options */
|
|
47
|
-
tokenEstimates: TokenEstimate[];
|
|
48
|
-
/** Whether ERC-20 token cost estimation is in progress */
|
|
49
|
-
estimatingTokenCosts: boolean;
|
|
50
|
-
/** Currently selected fee token */
|
|
51
|
-
selectedFeeToken: FeeTokenOption | null;
|
|
52
|
-
/** Function to manually select a fee token */
|
|
53
|
-
setSelectedFeeToken: (token: FeeTokenOption | null) => void;
|
|
54
|
-
/** Whether user is paying with ERC-20 (not ETH, not sponsored) */
|
|
55
|
-
isPayingWithErc20: boolean;
|
|
56
|
-
/** Re-estimate gas (useful after transaction changes) */
|
|
57
|
-
refetch: () => void;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ============================================================================
|
|
61
|
-
// Constants
|
|
62
|
-
// ============================================================================
|
|
63
|
-
|
|
64
|
-
/** Fallback gas estimate in ETH for L2 chains when ETH estimation fails */
|
|
65
|
-
const FALLBACK_GAS_ESTIMATE_ETH = '0.00005';
|
|
66
|
-
|
|
67
|
-
/** Error messages that indicate insufficient funds */
|
|
68
|
-
const INSUFFICIENT_FUNDS_ERRORS = [
|
|
69
|
-
'AA21',
|
|
70
|
-
"didn't pay prefund",
|
|
71
|
-
'insufficient',
|
|
72
|
-
'AA50', // PostOp reverted (e.g., paymaster insufficient balance)
|
|
73
|
-
'paymasterValidationGasLimit is required', // ERC-20 paymaster can't validate with 0 balance
|
|
74
|
-
];
|
|
75
|
-
|
|
76
|
-
// ============================================================================
|
|
77
|
-
// Helper Functions
|
|
78
|
-
// ============================================================================
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Check if an error indicates insufficient funds
|
|
82
|
-
*/
|
|
83
|
-
function isInsufficientFundsError(error: unknown): boolean {
|
|
84
|
-
if (!(error instanceof Error)) return false;
|
|
85
|
-
return INSUFFICIENT_FUNDS_ERRORS.some(msg =>
|
|
86
|
-
error.message.toLowerCase().includes(msg.toLowerCase())
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Build paymaster URL with chain ID and API key
|
|
92
|
-
*/
|
|
93
|
-
function buildPaymasterUrl(chainId: number, apiKey?: string): string {
|
|
94
|
-
const baseUrl = `${JAW_PAYMASTER_URL}?chainId=${chainId}`;
|
|
95
|
-
return apiKey ? `${baseUrl}&api-key=${apiKey}` : baseUrl;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ============================================================================
|
|
99
|
-
// Hook Implementation
|
|
100
|
-
// ============================================================================
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Hook for estimating gas costs in both ETH and ERC-20 tokens.
|
|
104
|
-
*
|
|
105
|
-
* Features:
|
|
106
|
-
* - Parallel estimation of ETH and ERC-20 costs using Promise.allSettled
|
|
107
|
-
* - Automatic fallback to ERC-20 when ETH balance is insufficient
|
|
108
|
-
* - Smart token selection based on available balances
|
|
109
|
-
* - Loading states for smooth UI transitions
|
|
110
|
-
*
|
|
111
|
-
* @example
|
|
112
|
-
* ```tsx
|
|
113
|
-
* const {
|
|
114
|
-
* gasFee,
|
|
115
|
-
* gasFeeLoading,
|
|
116
|
-
* selectedFeeToken,
|
|
117
|
-
* setSelectedFeeToken,
|
|
118
|
-
* isPayingWithErc20,
|
|
119
|
-
* } = useGasEstimation({
|
|
120
|
-
* account,
|
|
121
|
-
* transactionCalls,
|
|
122
|
-
* chainId: 84532,
|
|
123
|
-
* apiKey: 'your-api-key',
|
|
124
|
-
* feeTokens,
|
|
125
|
-
* onFeeTokensUpdate: setFeeTokens,
|
|
126
|
-
* });
|
|
127
|
-
* ```
|
|
128
|
-
*/
|
|
129
|
-
export function useGasEstimation({
|
|
130
|
-
account,
|
|
131
|
-
transactionCalls,
|
|
132
|
-
chainId,
|
|
133
|
-
apiKey,
|
|
134
|
-
feeTokens,
|
|
135
|
-
isSponsored = false,
|
|
136
|
-
permissionId,
|
|
137
|
-
onFeeTokensUpdate,
|
|
138
|
-
}: UseGasEstimationConfig): UseGasEstimationResult {
|
|
139
|
-
// -------------------------------------------------------------------------
|
|
140
|
-
// State
|
|
141
|
-
// -------------------------------------------------------------------------
|
|
142
|
-
|
|
143
|
-
const [gasFee, setGasFee] = useState<string>('');
|
|
144
|
-
const [gasFeeLoading, setGasFeeLoading] = useState<boolean>(true);
|
|
145
|
-
const [gasEstimationError, setGasEstimationError] = useState<string>('');
|
|
146
|
-
const [tokenEstimates, setTokenEstimates] = useState<TokenEstimate[]>([]);
|
|
147
|
-
const [estimatingTokenCosts, setEstimatingTokenCosts] = useState<boolean>(false);
|
|
148
|
-
const [selectedFeeToken, setSelectedFeeToken] = useState<FeeTokenOption | null>(null);
|
|
149
|
-
|
|
150
|
-
// Track estimation version to handle race conditions
|
|
151
|
-
const estimationVersionRef = useRef<number>(0);
|
|
152
|
-
|
|
153
|
-
// Use refs for values that shouldn't trigger re-estimation
|
|
154
|
-
const feeTokensRef = useRef(feeTokens);
|
|
155
|
-
feeTokensRef.current = feeTokens;
|
|
156
|
-
|
|
157
|
-
const onFeeTokensUpdateRef = useRef(onFeeTokensUpdate);
|
|
158
|
-
onFeeTokensUpdateRef.current = onFeeTokensUpdate;
|
|
159
|
-
|
|
160
|
-
// Track ERC-20 token addresses to re-run estimation when new tokens are added
|
|
161
|
-
// This is stable when only gasCostFormatted/isSelectable change (preventing infinite loops)
|
|
162
|
-
const erc20TokenAddresses = useMemo(
|
|
163
|
-
() => feeTokens
|
|
164
|
-
.filter(t => !t.isNative)
|
|
165
|
-
.map(t => t.address.toLowerCase())
|
|
166
|
-
.sort()
|
|
167
|
-
.join(','),
|
|
168
|
-
[feeTokens]
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
// -------------------------------------------------------------------------
|
|
172
|
-
// Derived State
|
|
173
|
-
// -------------------------------------------------------------------------
|
|
174
|
-
|
|
175
|
-
const isPayingWithErc20 = !isSponsored && !!selectedFeeToken && !selectedFeeToken.isNative;
|
|
176
|
-
|
|
177
|
-
// -------------------------------------------------------------------------
|
|
178
|
-
// Sync selected token when feeTokens update
|
|
179
|
-
// -------------------------------------------------------------------------
|
|
180
|
-
|
|
181
|
-
useEffect(() => {
|
|
182
|
-
if (!selectedFeeToken || selectedFeeToken.isNative) return;
|
|
183
|
-
|
|
184
|
-
const updatedToken = feeTokens.find(
|
|
185
|
-
t => t.address.toLowerCase() === selectedFeeToken.address.toLowerCase()
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
// Update if gasCostFormatted changed (new estimate came in)
|
|
189
|
-
if (updatedToken && updatedToken.gasCostFormatted !== selectedFeeToken.gasCostFormatted) {
|
|
190
|
-
setSelectedFeeToken(updatedToken);
|
|
191
|
-
}
|
|
192
|
-
}, [feeTokens, selectedFeeToken]);
|
|
193
|
-
|
|
194
|
-
// -------------------------------------------------------------------------
|
|
195
|
-
// Main Estimation Logic
|
|
196
|
-
// -------------------------------------------------------------------------
|
|
197
|
-
|
|
198
|
-
const estimateGas = useCallback(async () => {
|
|
199
|
-
// Validation - keep loading state true while waiting for account
|
|
200
|
-
if (!account) {
|
|
201
|
-
// Don't set loading to false here - we're still waiting for prerequisites
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Handle sponsored transactions
|
|
206
|
-
if (isSponsored) {
|
|
207
|
-
setGasFee('sponsored');
|
|
208
|
-
setGasFeeLoading(false);
|
|
209
|
-
setGasEstimationError('');
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Handle empty transactionCalls (e.g., for permission grants)
|
|
214
|
-
// Use a fallback gas estimate and still run ERC-20 estimation with a dummy call
|
|
215
|
-
// Using zero address as dummy target for estimation purposes
|
|
216
|
-
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as Address;
|
|
217
|
-
const effectiveCalls = transactionCalls.length > 0
|
|
218
|
-
? transactionCalls
|
|
219
|
-
: [{ to: ZERO_ADDRESS, value: 0n }]; // Dummy call for estimation
|
|
220
|
-
|
|
221
|
-
// Increment version to track this estimation
|
|
222
|
-
const currentVersion = ++estimationVersionRef.current;
|
|
223
|
-
|
|
224
|
-
// Start loading
|
|
225
|
-
setGasFeeLoading(true);
|
|
226
|
-
setEstimatingTokenCosts(true);
|
|
227
|
-
setGasEstimationError('');
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
// Use refs to avoid infinite loops (these values shouldn't trigger re-estimation)
|
|
231
|
-
const currentFeeTokens = feeTokensRef.current;
|
|
232
|
-
const erc20Tokens = currentFeeTokens.filter(t => !t.isNative);
|
|
233
|
-
// Filter tokens with balance > 0 for estimation (paymaster can't validate with 0 balance)
|
|
234
|
-
const erc20TokensWithBalance = erc20Tokens.filter(t => t.balance > 0n);
|
|
235
|
-
const paymasterUrl = buildPaymasterUrl(chainId, apiKey);
|
|
236
|
-
|
|
237
|
-
// Convert effectiveCalls to ensure value is bigint for estimateErc20PaymasterCosts
|
|
238
|
-
const callsWithBigIntValue = effectiveCalls.map(call => ({
|
|
239
|
-
to: call.to as Address,
|
|
240
|
-
value: call.value !== undefined
|
|
241
|
-
? (typeof call.value === 'string' ? BigInt(call.value) : call.value)
|
|
242
|
-
: undefined,
|
|
243
|
-
data: call.data as Hex | undefined,
|
|
244
|
-
}));
|
|
245
|
-
|
|
246
|
-
// Run ETH and ERC-20 estimation in parallel
|
|
247
|
-
const [ethResult, erc20Result] = await Promise.allSettled([
|
|
248
|
-
// ETH gas estimation
|
|
249
|
-
account.calculateGasCost(
|
|
250
|
-
effectiveCalls,
|
|
251
|
-
permissionId ? { permissionId } : undefined
|
|
252
|
-
),
|
|
253
|
-
// ERC-20 gas estimation (only for tokens with balance > 0)
|
|
254
|
-
// Tokens with 0 balance will be marked as not selectable below
|
|
255
|
-
erc20TokensWithBalance.length > 0
|
|
256
|
-
? estimateErc20PaymasterCosts(
|
|
257
|
-
account.getSmartAccount(),
|
|
258
|
-
callsWithBigIntValue,
|
|
259
|
-
account.getChain(),
|
|
260
|
-
paymasterUrl,
|
|
261
|
-
erc20TokensWithBalance.map(t => ({
|
|
262
|
-
address: t.address as Address,
|
|
263
|
-
symbol: t.symbol,
|
|
264
|
-
decimals: t.decimals,
|
|
265
|
-
balance: t.balance,
|
|
266
|
-
}))
|
|
267
|
-
)
|
|
268
|
-
: Promise.resolve([])
|
|
269
|
-
]);
|
|
270
|
-
|
|
271
|
-
// Check if this estimation is still current (handle race conditions)
|
|
272
|
-
if (currentVersion !== estimationVersionRef.current) {
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Process ERC-20 results first (so we have updated feeTokens for decision making)
|
|
277
|
-
let updatedFeeTokens = [...currentFeeTokens];
|
|
278
|
-
let erc20Estimates: TokenEstimate[] = [];
|
|
279
|
-
|
|
280
|
-
if (erc20Result.status === 'fulfilled') {
|
|
281
|
-
erc20Estimates = erc20Result.value;
|
|
282
|
-
setTokenEstimates(erc20Estimates);
|
|
283
|
-
|
|
284
|
-
// Update feeTokens with the estimated costs and selectability
|
|
285
|
-
updatedFeeTokens = currentFeeTokens.map(token => {
|
|
286
|
-
if (token.isNative) return token;
|
|
287
|
-
|
|
288
|
-
// Tokens with 0 balance are not selectable (can't pay gas fees)
|
|
289
|
-
if (token.balance === 0n) {
|
|
290
|
-
return {
|
|
291
|
-
...token,
|
|
292
|
-
gasCostFormatted: undefined, // No estimate available
|
|
293
|
-
isSelectable: false,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const estimate = erc20Estimates.find(
|
|
298
|
-
e => e.tokenAddress.toLowerCase() === token.address.toLowerCase()
|
|
299
|
-
);
|
|
300
|
-
|
|
301
|
-
if (estimate) {
|
|
302
|
-
return {
|
|
303
|
-
...token,
|
|
304
|
-
gasCostFormatted: estimate.tokenCostFormatted,
|
|
305
|
-
isSelectable: estimate.hasSufficientBalance,
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
return token;
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
// Notify parent of updated tokens (use ref to avoid infinite loop)
|
|
312
|
-
onFeeTokensUpdateRef.current?.(updatedFeeTokens);
|
|
313
|
-
} else if (erc20Result.status === 'rejected') {
|
|
314
|
-
// Check if this is an insufficient balance error (expected case, not a real error)
|
|
315
|
-
const isInsufficientBalance = isInsufficientFundsError(erc20Result.reason);
|
|
316
|
-
|
|
317
|
-
if (isInsufficientBalance) {
|
|
318
|
-
// This is an expected case - user doesn't have enough ERC-20 tokens
|
|
319
|
-
// Don't log as error, just mark tokens as insufficient
|
|
320
|
-
updatedFeeTokens = currentFeeTokens.map(token => {
|
|
321
|
-
if (token.isNative) return token;
|
|
322
|
-
|
|
323
|
-
// Tokens with 0 balance are not selectable
|
|
324
|
-
if (token.balance === 0n) {
|
|
325
|
-
return {
|
|
326
|
-
...token,
|
|
327
|
-
gasCostFormatted: undefined,
|
|
328
|
-
isSelectable: false,
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Try to extract the required amount from the error message
|
|
333
|
-
// Format: "X.XXX USDC required but sender has Y USDC"
|
|
334
|
-
let gasCostFormatted = 'Insufficient';
|
|
335
|
-
const errorMsg = erc20Result.reason instanceof Error ? erc20Result.reason.message : '';
|
|
336
|
-
const match = errorMsg.match(/([\d.]+)\s*(\w+)\s*required/i);
|
|
337
|
-
if (match) {
|
|
338
|
-
gasCostFormatted = match[1]; // Just the amount, symbol is already shown
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return {
|
|
342
|
-
...token,
|
|
343
|
-
gasCostFormatted,
|
|
344
|
-
isSelectable: false,
|
|
345
|
-
};
|
|
346
|
-
});
|
|
347
|
-
} else {
|
|
348
|
-
// Unexpected error - log it and show estimation failed
|
|
349
|
-
console.error('[useGasEstimation] ERC-20 estimation failed:', erc20Result.reason);
|
|
350
|
-
updatedFeeTokens = currentFeeTokens.map(token => {
|
|
351
|
-
if (token.isNative) return token;
|
|
352
|
-
|
|
353
|
-
// Tokens with 0 balance - just mark as not selectable without error message
|
|
354
|
-
if (token.balance === 0n) {
|
|
355
|
-
return {
|
|
356
|
-
...token,
|
|
357
|
-
gasCostFormatted: undefined,
|
|
358
|
-
isSelectable: false,
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return {
|
|
363
|
-
...token,
|
|
364
|
-
gasCostFormatted: 'Estimation failed',
|
|
365
|
-
isSelectable: false,
|
|
366
|
-
};
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
onFeeTokensUpdateRef.current?.(updatedFeeTokens);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Process ETH result
|
|
373
|
-
const ethSuccess = ethResult.status === 'fulfilled';
|
|
374
|
-
const ethInsufficientFunds = ethResult.status === 'rejected' &&
|
|
375
|
-
isInsufficientFundsError(ethResult.reason);
|
|
376
|
-
|
|
377
|
-
if (ethSuccess) {
|
|
378
|
-
handleEthSuccess(ethResult.value, updatedFeeTokens);
|
|
379
|
-
} else if (ethInsufficientFunds) {
|
|
380
|
-
handleEthInsufficientFunds(updatedFeeTokens);
|
|
381
|
-
} else {
|
|
382
|
-
handleEstimationError(ethResult.status === 'rejected' ? ethResult.reason : null);
|
|
383
|
-
}
|
|
384
|
-
} catch (error) {
|
|
385
|
-
// Check if this estimation is still current
|
|
386
|
-
if (currentVersion !== estimationVersionRef.current) {
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
handleEstimationError(error);
|
|
390
|
-
} finally {
|
|
391
|
-
// Only update loading states if this is still the current estimation
|
|
392
|
-
if (currentVersion === estimationVersionRef.current) {
|
|
393
|
-
setGasFeeLoading(false);
|
|
394
|
-
setEstimatingTokenCosts(false);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
// Note: feeTokens and onFeeTokensUpdate accessed via refs to prevent infinite loops
|
|
398
|
-
// erc20TokenAddresses triggers re-estimation when new ERC-20 tokens are added (but not when estimates update)
|
|
399
|
-
}, [account, transactionCalls, chainId, apiKey, isSponsored, permissionId, erc20TokenAddresses]);
|
|
400
|
-
|
|
401
|
-
// -------------------------------------------------------------------------
|
|
402
|
-
// Result Handlers
|
|
403
|
-
// -------------------------------------------------------------------------
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Handle successful ETH gas estimation
|
|
407
|
-
*/
|
|
408
|
-
const handleEthSuccess = useCallback((gasPrice: string, updatedFeeTokens: FeeTokenOption[]) => {
|
|
409
|
-
setGasFee(gasPrice);
|
|
410
|
-
setGasEstimationError('');
|
|
411
|
-
|
|
412
|
-
// Auto-select native token if not already selected
|
|
413
|
-
if (!selectedFeeToken) {
|
|
414
|
-
const nativeToken = updatedFeeTokens.find(t => t.isNative && t.isSelectable);
|
|
415
|
-
if (nativeToken) {
|
|
416
|
-
setSelectedFeeToken(nativeToken);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}, [selectedFeeToken]);
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Handle ETH insufficient funds - try to fallback to ERC-20
|
|
423
|
-
*/
|
|
424
|
-
const handleEthInsufficientFunds = useCallback((
|
|
425
|
-
updatedFeeTokens: FeeTokenOption[],
|
|
426
|
-
) => {
|
|
427
|
-
const selectableErc20 = updatedFeeTokens.find(t => !t.isNative && t.isSelectable);
|
|
428
|
-
|
|
429
|
-
if (selectableErc20) {
|
|
430
|
-
// Auto-select first selectable ERC-20 token
|
|
431
|
-
setSelectedFeeToken(selectableErc20);
|
|
432
|
-
setGasFee(FALLBACK_GAS_ESTIMATE_ETH);
|
|
433
|
-
setGasEstimationError('');
|
|
434
|
-
} else {
|
|
435
|
-
// No selectable payment options (neither ETH nor ERC-20 have sufficient balance)
|
|
436
|
-
setGasFee('');
|
|
437
|
-
setGasEstimationError('Insufficient funds');
|
|
438
|
-
}
|
|
439
|
-
}, []);
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Handle estimation error (not insufficient funds)
|
|
443
|
-
*/
|
|
444
|
-
const handleEstimationError = useCallback((error: unknown) => {
|
|
445
|
-
console.error('[useGasEstimation] Error:', error);
|
|
446
|
-
setGasFee('');
|
|
447
|
-
setGasEstimationError('Failed to estimate gas');
|
|
448
|
-
}, []);
|
|
449
|
-
|
|
450
|
-
// -------------------------------------------------------------------------
|
|
451
|
-
// Effects
|
|
452
|
-
// -------------------------------------------------------------------------
|
|
453
|
-
|
|
454
|
-
// Run estimation when dependencies change
|
|
455
|
-
useEffect(() => {
|
|
456
|
-
estimateGas();
|
|
457
|
-
}, [estimateGas]);
|
|
458
|
-
|
|
459
|
-
// -------------------------------------------------------------------------
|
|
460
|
-
// Return
|
|
461
|
-
// -------------------------------------------------------------------------
|
|
462
|
-
|
|
463
|
-
return {
|
|
464
|
-
gasFee,
|
|
465
|
-
gasFeeLoading,
|
|
466
|
-
gasEstimationError,
|
|
467
|
-
tokenEstimates,
|
|
468
|
-
estimatingTokenCosts,
|
|
469
|
-
selectedFeeToken,
|
|
470
|
-
setSelectedFeeToken,
|
|
471
|
-
isPayingWithErc20,
|
|
472
|
-
refetch: estimateGas,
|
|
473
|
-
};
|
|
474
|
-
}
|
package/src/hooks/useIsMobile.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
|
|
3
|
-
export function useIsMobile(breakpoint = 768) {
|
|
4
|
-
const [isMobile, setIsMobile] = useState(false)
|
|
5
|
-
|
|
6
|
-
useEffect(() => {
|
|
7
|
-
// Check if window is defined (client-side)
|
|
8
|
-
if (typeof window === 'undefined') return undefined
|
|
9
|
-
|
|
10
|
-
// Media query for mobile detection
|
|
11
|
-
const mediaQuery = window.matchMedia(`(max-width: ${breakpoint - 1}px)`)
|
|
12
|
-
|
|
13
|
-
// Set initial value
|
|
14
|
-
setIsMobile(mediaQuery.matches)
|
|
15
|
-
|
|
16
|
-
// Handler for media query changes
|
|
17
|
-
const handleMediaQueryChange = (e: MediaQueryListEvent) => {
|
|
18
|
-
setIsMobile(e.matches)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Modern browsers
|
|
22
|
-
if (mediaQuery.addEventListener) {
|
|
23
|
-
mediaQuery.addEventListener('change', handleMediaQueryChange)
|
|
24
|
-
return () => mediaQuery.removeEventListener('change', handleMediaQueryChange)
|
|
25
|
-
}
|
|
26
|
-
// Legacy browsers
|
|
27
|
-
else if (mediaQuery.addListener) {
|
|
28
|
-
mediaQuery.addListener(handleMediaQueryChange)
|
|
29
|
-
return () => mediaQuery.removeListener(handleMediaQueryChange)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return undefined
|
|
33
|
-
}, [breakpoint])
|
|
34
|
-
|
|
35
|
-
return isMobile
|
|
36
|
-
}
|