@ensofinance/checkout-widget 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/checkout-widget.es.js +25523 -24215
  2. package/dist/checkout-widget.es.js.map +1 -1
  3. package/dist/checkout-widget.umd.js +64 -59
  4. package/dist/checkout-widget.umd.js.map +1 -1
  5. package/dist/index.d.ts +5 -1
  6. package/package.json +1 -1
  7. package/src/assets/providers/alchemypay.svg +21 -0
  8. package/src/assets/providers/banxa.svg +21 -0
  9. package/src/assets/providers/binanceconnect.svg +14 -0
  10. package/src/assets/providers/kryptonim.svg +6 -0
  11. package/src/assets/providers/mercuryo.svg +21 -0
  12. package/src/assets/providers/moonpay.svg +14 -0
  13. package/src/assets/providers/stripe.svg +16 -0
  14. package/src/assets/providers/swapped.svg +1 -0
  15. package/src/assets/providers/topper.svg +14 -0
  16. package/src/assets/providers/transak.svg +21 -0
  17. package/src/assets/providers/unlimit.svg +21 -0
  18. package/src/components/AmountInput.tsx +41 -25
  19. package/src/components/ChakraProvider.tsx +36 -13
  20. package/src/components/Checkout.tsx +7 -1
  21. package/src/components/CurrencySwapDisplay.tsx +59 -22
  22. package/src/components/DepositProcessing.tsx +1 -1
  23. package/src/components/ExchangeConfirmSecurity.tsx +1 -1
  24. package/src/components/QuoteParameters.tsx +1 -1
  25. package/src/components/TransactionDetailRow.tsx +2 -2
  26. package/src/components/cards/ExchangeCard.tsx +1 -1
  27. package/src/components/cards/OptionCard.tsx +2 -1
  28. package/src/components/cards/WalletCard.tsx +1 -1
  29. package/src/components/modal.tsx +3 -3
  30. package/src/components/steps/CardBuyFlow/CardBuyFlow.tsx +412 -0
  31. package/src/components/steps/CardBuyFlow/ChooseAmountStep.tsx +352 -0
  32. package/src/components/steps/CardBuyFlow/OpenWidgetStep.tsx +193 -0
  33. package/src/components/steps/ExchangeFlow.tsx +254 -1416
  34. package/src/components/steps/FlowSelector.tsx +117 -60
  35. package/src/components/steps/SmartAccountFlow.tsx +372 -0
  36. package/src/components/steps/WalletFlow/WalletAmountStep.tsx +2 -2
  37. package/src/components/steps/WalletFlow/WalletConfirmStep.tsx +92 -51
  38. package/src/components/steps/WalletFlow/WalletFlow.tsx +17 -16
  39. package/src/components/steps/WalletFlow/WalletQuoteStep.tsx +2 -2
  40. package/src/components/steps/WalletFlow/WalletTokenStep.tsx +6 -4
  41. package/src/components/steps/shared/ChooseAmountStep.tsx +325 -0
  42. package/src/components/steps/shared/SignUserOpStep.tsx +117 -0
  43. package/src/components/steps/shared/TrackUserOpStep.tsx +625 -0
  44. package/src/components/steps/shared/exchangeIntegration.ts +19 -0
  45. package/src/components/steps/shared/types.ts +22 -0
  46. package/src/components/ui/index.tsx +23 -6
  47. package/src/components/ui/toaster.tsx +2 -1
  48. package/src/components/ui/transitions.tsx +16 -0
  49. package/src/types/index.ts +99 -0
  50. package/src/util/constants.tsx +27 -0
  51. package/src/util/enso-hooks.tsx +75 -61
  52. package/src/util/meld-hooks.tsx +533 -0
  53. package/src/assets/usdc.webp +0 -0
  54. package/src/assets/usdt.webp +0 -0
@@ -5,14 +5,17 @@ import {
5
5
  Icon,
6
6
  Text,
7
7
  Flex,
8
- Skeleton,
9
- Image,
10
- Table,
11
8
  } from "@chakra-ui/react";
12
9
  import { ChevronLeft, X, TriangleAlert } from "lucide-react";
13
- import { useContext, useEffect, useMemo, useState, useCallback } from "react";
14
- import { useAccount, useSignMessage } from "wagmi";
15
- import { getUserOperationHash } from "viem/account-abstraction";
10
+ import {
11
+ useContext,
12
+ useEffect,
13
+ useMemo,
14
+ useState,
15
+ useCallback,
16
+ } from "react";
17
+ import { useQuery } from "@tanstack/react-query";
18
+ import { useAccount } from "wagmi";
16
19
  import {
17
20
  BodyWrapper,
18
21
  HeaderDescription,
@@ -20,10 +23,13 @@ import {
20
23
  HeaderWrapper,
21
24
  ListWrapper,
22
25
  } from "../ui/styled";
23
- import { IconButton, Button, Input } from "../ui";
24
- import { AmountInput, AmountInputValue } from "../AmountInput";
26
+ import { IconButton, Button } from "../ui";
25
27
  import { CheckoutContext } from "../Checkout";
26
28
  import Modal from "../modal";
29
+ import TrackUserOpStep from "@/components/steps/shared/TrackUserOpStep";
30
+ import SharedChooseAmountStep from "@/components/steps/shared/ChooseAmountStep";
31
+ import SharedSignUserOpStep from "@/components/steps/shared/SignUserOpStep";
32
+ import { AnimatedStep } from "../ui/transitions";
27
33
  import {
28
34
  AccessTokenPayload,
29
35
  createLink,
@@ -32,69 +38,26 @@ import {
32
38
  import { useAppStore } from "@/store";
33
39
  import { AssetCard } from "../cards";
34
40
  import {
35
- denormalizeValue,
36
41
  formatNumber,
37
42
  formatUSD,
38
43
  normalizeValue,
39
44
  } from "@/util";
45
+ import { useTokenFromListBySymbols } from "@/util/common";
40
46
  import {
41
- useTokenFromListBySymbols,
42
- precisionizeNumber,
43
- getPositiveDecimalValue,
44
- } from "@/util/common";
45
- import {
46
- EXCHANGE_MAX_LIMIT_GAP_USD,
47
- EXCHANGE_MIN_LIMIT,
48
47
  getCexIntermediateChain,
49
48
  DEFAULT_CEX_BRIDGE_CHAIN_MAPPING,
49
+ CHECKOUT_BFF_URL,
50
50
  } from "@/util/constants";
51
- import {
52
- useAppDetails,
53
- useRouteData,
54
- useSmartAccountBalances,
55
- } from "@/util/enso-hooks";
56
- import QuoteParameters from "../QuoteParameters";
57
- import { TransactionDetailRow } from "../TransactionDetailRow";
58
- import { CircleTimer } from "../CircleTimer";
51
+ import { useAppDetails } from "@/util/enso-hooks";
59
52
  import { ConfirmExchangeStep } from "../ExchangeConfirmSecurity";
53
+ import {
54
+ ExchangeToIntegrationType,
55
+ EXCHANGE_ICON_BY_TYPE,
56
+ } from "./shared/exchangeIntegration";
57
+ import type { MatchedToken, CryptocurrencyPosition, SupportedToken } from "./shared/types";
60
58
 
61
- import SuccessIcon from "@/assets/success.svg";
62
- import FailIcon from "@/assets/fail.svg";
63
- import { SupportedExchanges } from "../../types";
64
- import { useLayerZeroStatus } from "@/util/tx-tracker";
65
- import { STARGATE_CHAIN_NAMES, CHAINS_ETHERSCAN } from "@/util/constants";
66
-
67
- const ENTRY_POINT_ADDRESS: `0x${string}` =
68
- "0x0000000071727de22e5e9d8baf0edac6f37da032";
69
-
70
- export const ExchangeToIntegrationType: Record<SupportedExchanges, string> = {
71
- [SupportedExchanges.Binance]: "binanceInternationalDirect",
72
- [SupportedExchanges.Kraken]: "krakenDirect",
73
- [SupportedExchanges.Coinbase]: "coinbase",
74
- [SupportedExchanges.Bybit]: "bybitDirect",
75
- };
76
-
77
- // Map Mesh broker types to icon URLs
78
- export const EXCHANGE_ICON_BY_TYPE: Record<string, string> = {
79
- [ExchangeToIntegrationType[SupportedExchanges.Binance]]:
80
- "https://assets.coingecko.com/markets/images/52/large/binance.jpg",
81
- [ExchangeToIntegrationType[SupportedExchanges.Kraken]]:
82
- "https://assets.coingecko.com/markets/images/29/large/kraken.jpg",
83
- [ExchangeToIntegrationType[SupportedExchanges.Coinbase]]:
84
- "https://assets.coingecko.com/markets/images/23/large/Coinbase_Coin_Primary.png",
85
- [ExchangeToIntegrationType[SupportedExchanges.Bybit]]:
86
- "https://assets.coingecko.com/markets/images/698/large/bybit_spot.png",
87
- };
88
59
 
89
60
  // Types for Mesh Holdings API response
90
- interface CryptocurrencyPosition {
91
- marketValue: number;
92
- lastPrice: number;
93
- name: string;
94
- symbol: string;
95
- amount: number;
96
- }
97
-
98
61
  interface HoldingsContent {
99
62
  equityPositions: any[];
100
63
  cryptocurrencyPositions: CryptocurrencyPosition[];
@@ -119,59 +82,38 @@ interface HoldingsResponse {
119
82
  errorType: string;
120
83
  }
121
84
 
122
- interface SupportedToken {
123
- symbol: string;
124
- name: string;
125
- networkId: string;
126
- chainId: number;
127
- integrationNetworks: any[];
128
- }
129
-
130
- interface MatchedToken extends SupportedToken {
131
- balance: number;
132
- marketValue: number;
133
- holding?: CryptocurrencyPosition;
134
- }
135
-
136
- const isDelayedBalanceUsed = (integrationType: string) =>
137
- integrationType === "delayed";
138
-
139
- // const MESH_API_URL = "http://localhost:8787";
140
- const MESH_API_URL = "https://mesh-bff.enso-checkout.workers.dev";
85
+ type MeshRequestError = Error & {
86
+ code?: string;
87
+ };
141
88
 
142
89
  /*
143
90
  Withdrawal steps:
144
- 1. Check if session key is available
145
- 2. Perform auth if not availble (optional)
91
+ 1. Check if session key is available
92
+ 2. Perform auth if not available (optional)
146
93
  3. Get holdings and show token selector
147
94
  4. Select amount
148
- 6. Get userOp signature
149
- 7. Open transfer modal with amount and token
95
+ 5. Get userOp signature
96
+ 6. Open transfer modal with amount and token
150
97
  */
151
98
 
152
99
  export enum WithdrawalStep {
153
- CheckSessionKey,
154
100
  ChooseExchange,
101
+ CheckSessionKey,
155
102
  ChooseExchangeAsset,
156
- ChooseBalanceAsset,
157
103
  ChooseAmount,
158
104
  SignUserOp,
159
105
  InitiateWithdrawal,
160
106
  TrackUserOp,
161
107
  }
162
- const withdrawalSteps = [
163
- WithdrawalStep.ChooseExchange,
164
- WithdrawalStep.ChooseExchangeAsset,
165
- WithdrawalStep.ChooseAmount,
166
- WithdrawalStep.SignUserOp,
167
- WithdrawalStep.InitiateWithdrawal,
168
- ];
169
- const balanceSteps = [
170
- WithdrawalStep.ChooseBalanceAsset,
171
- WithdrawalStep.ChooseAmount,
172
- WithdrawalStep.SignUserOp,
173
- ];
174
- // Integration details are fetched dynamically from Mesh API.
108
+
109
+ const withdrawalPreviousStep: Partial<Record<WithdrawalStep, WithdrawalStep>> =
110
+ {
111
+ [WithdrawalStep.CheckSessionKey]: WithdrawalStep.ChooseExchange,
112
+ [WithdrawalStep.ChooseExchangeAsset]: WithdrawalStep.ChooseExchange,
113
+ [WithdrawalStep.ChooseAmount]: WithdrawalStep.ChooseExchangeAsset,
114
+ [WithdrawalStep.SignUserOp]: WithdrawalStep.ChooseAmount,
115
+ [WithdrawalStep.InitiateWithdrawal]: WithdrawalStep.SignUserOp,
116
+ };
175
117
 
176
118
  // Mesh network IDs for EVM chains (from Mesh networks API)
177
119
  const MESH_NETWORK_IDS: { [chainId: number]: string } = {
@@ -183,8 +125,6 @@ const MESH_NETWORK_IDS: { [chainId: number]: string } = {
183
125
  43114: "bad16371-c22a-4bf4-a311-274d046cd760", // Avalanche C-Chain
184
126
  56: "ed0ebeec-b166-4c8b-8574-cb078f7af8cf", // BSC
185
127
  146: "385f0b3a-8471-4b8f-884f-c4f4496f1603", // Sonic
186
- // 81457: "0c17e03f-77fa-4644-b84c-eb247af8c4c1", // Blast
187
- // 11155111: "03b2d786-7092-4a6a-9737-d6013e21819b", // Sepolia (testnet)
188
128
  };
189
129
 
190
130
  const MESH_NETWORKS = Object.keys(MESH_NETWORK_IDS).map(Number);
@@ -193,6 +133,20 @@ const getNetworkId = (chainId: number): string => {
193
133
  return MESH_NETWORK_IDS[chainId] || MESH_NETWORK_IDS[8453]; // Default to Base
194
134
  };
195
135
 
136
+ const DEVICE_ID_KEY = "meshDeviceId";
137
+ const useDeviceId = () => {
138
+ return useMemo(() => {
139
+ let deviceId = localStorage.getItem(DEVICE_ID_KEY);
140
+
141
+ if (!deviceId) {
142
+ deviceId = `device_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
143
+ localStorage.setItem(DEVICE_ID_KEY, deviceId);
144
+ }
145
+
146
+ return deviceId;
147
+ }, []);
148
+ };
149
+
196
150
  const useHandleMeshAccessPayload = () => {
197
151
  const { setMeshAccessToken } = useAppStore();
198
152
  const deviceKey = useDeviceId();
@@ -200,12 +154,12 @@ const useHandleMeshAccessPayload = () => {
200
154
 
201
155
  return useCallback(
202
156
  (accessTokenPayload: AccessTokenPayload, sessionId: string) => {
203
- setMeshAccessToken(accessTokenPayload); // Persist access token and session id for future reloads
157
+ setMeshAccessToken(accessTokenPayload);
204
158
 
205
159
  sessionStorage.setItem(
206
160
  `${deviceKey}:${selectedIntegration?.type}`,
207
161
  JSON.stringify({
208
- accessTokenPayload, // Store full object for proper restoration
162
+ accessTokenPayload,
209
163
  sessionId,
210
164
  timestamp: Date.now(),
211
165
  }),
@@ -217,7 +171,7 @@ const useHandleMeshAccessPayload = () => {
217
171
 
218
172
  type MeshIntegration = {
219
173
  id: string;
220
- type: string; // brokerType
174
+ type: string;
221
175
  name: string;
222
176
  networks?: {
223
177
  id: string;
@@ -233,9 +187,6 @@ const ChooseExchangeStep = ({
233
187
  setStep: (step: WithdrawalStep) => void;
234
188
  }) => {
235
189
  const { chainIdOut, setChainIdIn } = useAppStore();
236
- const [integrations, setIntegrations] = useState<MeshIntegration[]>([]);
237
- const [loading, setLoading] = useState(true);
238
- const [error, setError] = useState<string | null>(null);
239
190
  const setSelectedIntegration = useAppStore(
240
191
  (state) => state.setSelectedIntegration,
241
192
  );
@@ -244,42 +195,50 @@ const ChooseExchangeStep = ({
244
195
  const cexMapping =
245
196
  cexBridgeChainMapping ?? DEFAULT_CEX_BRIDGE_CHAIN_MAPPING;
246
197
 
247
- // Use intermediate chain for filtering if target chain needs bridging
248
198
  const effectiveChainId =
249
199
  getCexIntermediateChain(chainIdOut, cexMapping) ?? chainIdOut;
250
200
 
251
- // Set chainIdIn to effective chain for cross-chain tracking
201
+ const availableExchanges = useMemo(
202
+ () =>
203
+ (enableExchange ?? [])
204
+ .map((exchange) => ExchangeToIntegrationType[exchange])
205
+ .filter(Boolean),
206
+ [enableExchange],
207
+ );
208
+
252
209
  useEffect(() => {
253
210
  effectiveChainId ? setChainIdIn(effectiveChainId) : chainIdOut;
254
211
  }, [effectiveChainId, setChainIdIn]);
255
212
 
256
- useEffect(() => {
257
- const fetchIntegrations = async () => {
258
- try {
259
- const res = await fetch(`${MESH_API_URL}/integrations`);
260
- const data = await res.json();
261
- const availableExchanges = enableExchange
262
- .map((e) => ExchangeToIntegrationType[e])
263
- .filter(Boolean);
264
-
265
- // Filter integrations by effective chain support (intermediate chain if bridging)
266
- const filtered = data?.filter(
267
- (i) =>
268
- i.networks.some(
269
- (n) => +n.chainId === effectiveChainId,
270
- ) && availableExchanges.includes(i.type),
271
- );
272
-
273
- setIntegrations(filtered);
274
- } catch (e) {
275
- console.error("Failed to fetch integrations", e);
276
- setError("Failed to load exchanges");
277
- } finally {
278
- setLoading(false);
213
+ const {
214
+ data: integrations = [],
215
+ isLoading: loading,
216
+ error,
217
+ } = useQuery({
218
+ queryKey: [
219
+ "mesh-integrations",
220
+ effectiveChainId,
221
+ ...availableExchanges,
222
+ ],
223
+ queryFn: async (): Promise<MeshIntegration[]> => {
224
+ if (!effectiveChainId || availableExchanges.length === 0) return [];
225
+
226
+ const res = await fetch(`${CHECKOUT_BFF_URL}/integrations`);
227
+ if (!res.ok) {
228
+ throw new Error("Failed to load exchanges");
279
229
  }
280
- };
281
- fetchIntegrations();
282
- }, [effectiveChainId]);
230
+
231
+ const data = (await res.json()) as MeshIntegration[];
232
+
233
+ return (data ?? []).filter(
234
+ (integration) =>
235
+ integration.networks?.some(
236
+ (network) => +network.chainId === effectiveChainId,
237
+ ) && availableExchanges.includes(integration.type),
238
+ );
239
+ },
240
+ staleTime: 60_000,
241
+ });
283
242
 
284
243
  if (loading)
285
244
  return (
@@ -291,23 +250,18 @@ const ChooseExchangeStep = ({
291
250
  return (
292
251
  <BodyWrapper>
293
252
  <Box p={5} color="red.500">
294
- {error}
253
+ Failed to load exchanges
295
254
  </Box>
296
255
  </BodyWrapper>
297
256
  );
298
257
 
299
258
  return (
300
259
  <BodyWrapper>
301
- {/*<Box mb={4} width="100%" textAlign="left">*/}
302
- {/* <HeaderTitle>Choose Exchange</HeaderTitle>*/}
303
- {/*</Box>*/}
304
-
305
260
  {integrations?.length > 0 ? (
306
261
  <ListWrapper>
307
262
  {integrations.map((integration) => (
308
263
  <AssetCard
309
264
  key={integration.id}
310
- // chainId={chainIdOut || 1}
311
265
  icon={EXCHANGE_ICON_BY_TYPE[integration.type]}
312
266
  title={integration.name}
313
267
  balance={""}
@@ -352,26 +306,20 @@ const CheckSessionKeyStep = ({
352
306
  const [showConfirmation, setShowConfirmation] = useState(false);
353
307
  const selectedIntegration = useAppStore((s) => s.selectedIntegration);
354
308
 
355
- // Bridging is required if chainIdIn differs from chainIdOut
356
309
  const needsBridging = chainIdIn !== chainIdOut;
357
-
358
- // Invalid only if chain is not in MESH_NETWORKS AND cannot be bridged
359
310
  const invalidChainId =
360
311
  chainIdOut && !MESH_NETWORKS.includes(chainIdOut) && !needsBridging;
361
312
  const handleMeshAccessPayload = useHandleMeshAccessPayload();
362
313
 
363
314
  useEffect(() => {
364
315
  if (!selectedIntegration) {
365
- // ensure an exchange is selected
366
316
  setStep(WithdrawalStep.ChooseExchange);
367
317
  return;
368
318
  }
369
319
  if (invalidChainId) return;
370
- // If connection is persisted, skip fetching a new link token
371
320
  const saved = sessionStorage.getItem(
372
321
  `${deviceKey}:${selectedIntegration.type}`,
373
322
  );
374
- // On load: check for persisted connection and hydrate state
375
323
  if (saved) {
376
324
  try {
377
325
  const parsed = JSON.parse(saved);
@@ -382,19 +330,17 @@ const CheckSessionKeyStep = ({
382
330
  return;
383
331
  }
384
332
  } catch (e) {
385
- // ignore malformed storage
386
333
  console.error("Failed to parse saved Mesh connection", e);
387
334
  }
388
335
  }
389
336
 
390
- // Show confirmation instead of auto-connecting
391
337
  setShowConfirmation(true);
392
338
  }, [deviceKey, invalidChainId, selectedIntegration]);
393
339
 
394
340
  const handleConfirmAuth = () => {
395
341
  const brokerType = selectedIntegration?.type;
396
342
  fetch(
397
- `${MESH_API_URL}/linktoken?brokerType=${encodeURIComponent(brokerType)}`,
343
+ `${CHECKOUT_BFF_URL}/linktoken?brokerType=${encodeURIComponent(brokerType)}`,
398
344
  {
399
345
  method: "POST",
400
346
  headers: {
@@ -420,7 +366,7 @@ const CheckSessionKeyStep = ({
420
366
  handleMeshAccessPayload(
421
367
  payload.accessToken,
422
368
  response.content.sessionId,
423
- ); // Persist access token and session id for future reloads
369
+ );
424
370
  },
425
371
  onExit: (error) => {
426
372
  console.log("onExit", error);
@@ -477,21 +423,6 @@ const CheckSessionKeyStep = ({
477
423
  return <Spinner m={5} />;
478
424
  };
479
425
 
480
- const DEVICE_ID_KEY = "meshDeviceId";
481
- // Generate a unique device ID to use as user id for Mesh
482
- const useDeviceId = () => {
483
- return useMemo(() => {
484
- let deviceId = localStorage.getItem(DEVICE_ID_KEY);
485
-
486
- if (!deviceId) {
487
- deviceId = `device_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
488
- localStorage.setItem(DEVICE_ID_KEY, deviceId);
489
- }
490
-
491
- return deviceId;
492
- }, []);
493
- };
494
-
495
426
  const ChooseAssetStep = ({
496
427
  setStep,
497
428
  onTokenSelect,
@@ -499,17 +430,9 @@ const ChooseAssetStep = ({
499
430
  setStep: (step: WithdrawalStep) => void;
500
431
  onTokenSelect: (token: MatchedToken) => void;
501
432
  }) => {
502
- // const [holdings, setHoldings] = useState<CryptocurrencyPosition[]>([]);
503
- // const [supportedTokens, setSupportedTokens] = useState<SupportedToken[]>(
504
- // [],
505
- // );
506
- const [matchedTokens, setMatchedTokens] = useState<MatchedToken[]>([]);
507
433
  const [selectedTokenSymbol, setSelectedTokenSymbol] = useState<
508
434
  string | null
509
435
  >(null);
510
- const [loading, setLoading] = useState(true);
511
- const [error, setError] = useState<string | null>(null);
512
- const { address } = useAccount();
513
436
  const {
514
437
  meshAccessToken,
515
438
  sessionId,
@@ -522,108 +445,115 @@ const ChooseAssetStep = ({
522
445
 
523
446
  const selectedIntegration = useAppStore((s) => s.selectedIntegration);
524
447
 
525
- useEffect(() => {
526
- const fetchData = async () => {
527
- try {
528
- // Fetch holdings for the selected exchange
529
- const holdingsResponse = await fetch(
530
- `${MESH_API_URL}/holdings`,
531
- {
532
- method: "POST",
533
- headers: {
534
- "Content-Type": "application/json",
535
- "x-session-id": sessionId,
536
- },
537
- body: JSON.stringify({
538
- authToken:
539
- meshAccessToken?.accountTokens?.[0]
540
- ?.accessToken,
541
- brokerType: selectedIntegration?.type,
542
- }),
448
+ const {
449
+ data: matchedTokens = [],
450
+ isLoading: loading,
451
+ error,
452
+ } = useQuery({
453
+ queryKey: [
454
+ "mesh-matched-tokens",
455
+ sessionId,
456
+ chainIdIn,
457
+ selectedIntegration?.type,
458
+ meshAccessToken?.accountTokens?.[0]?.accessToken,
459
+ ],
460
+ queryFn: async (): Promise<MatchedToken[]> => {
461
+ const holdingsResponse = await fetch(
462
+ `${CHECKOUT_BFF_URL}/holdings`,
463
+ {
464
+ method: "POST",
465
+ headers: {
466
+ "Content-Type": "application/json",
467
+ "x-session-id": sessionId || "",
543
468
  },
544
- );
545
-
546
- const holdingsData: HoldingsResponse =
547
- await holdingsResponse.json();
548
- console.log("Holdings data:", holdingsData);
549
-
550
- if (
551
- holdingsData.status !== "ok" ||
552
- !holdingsData.content?.cryptocurrencyPositions
553
- ) {
554
- if (holdingsData.errorType === "invalidIntegrationToken") {
555
- console.log("Invalid integration token");
556
- setStep(WithdrawalStep.CheckSessionKey);
557
- setMeshAccessToken(null);
558
- setSessionId(null);
559
- sessionStorage.removeItem(
560
- `${deviceKey}:${selectedIntegration?.type}`,
561
- );
562
- }
563
- throw new Error(
564
- holdingsData.message || "Failed to fetch holdings",
565
- );
469
+ body: JSON.stringify({
470
+ authToken:
471
+ meshAccessToken?.accountTokens?.[0]?.accessToken,
472
+ brokerType: selectedIntegration?.type,
473
+ }),
474
+ },
475
+ );
476
+ const holdingsData =
477
+ (await holdingsResponse.json()) as HoldingsResponse;
478
+ console.log("Holdings data:", holdingsData);
479
+
480
+ if (
481
+ holdingsData.status !== "ok" ||
482
+ !holdingsData.content?.cryptocurrencyPositions
483
+ ) {
484
+ const meshError = new Error(
485
+ holdingsData.message || "Failed to fetch holdings",
486
+ ) as MeshRequestError;
487
+ if (holdingsData.errorType === "invalidIntegrationToken") {
488
+ meshError.code = "invalidIntegrationToken";
566
489
  }
490
+ throw meshError;
491
+ }
567
492
 
568
- // Fetch supported tokens for chainIdIn (intermediate chain if bridging)
569
- const tokensResponse = await fetch(
570
- `${MESH_API_URL}/tokens?chainId=${chainIdIn}&brokerType=${encodeURIComponent(selectedIntegration?.type || "")}`,
571
- );
572
- const tokensData = await tokensResponse.json();
573
-
574
- if (
575
- tokensData.status === "success" &&
576
- tokensData.content?.tokens
577
- ) {
578
- // setSupportedTokens(tokensData.content.tokens);
579
-
580
- // Match holdings with supported tokens
581
- const matched = tokensData.content.tokens
582
- .map((token: SupportedToken) => {
583
- const holding =
584
- holdingsData.content.cryptocurrencyPositions.find(
585
- (h: CryptocurrencyPosition) =>
586
- h.symbol === token.symbol,
587
- );
588
- if (holding) {
589
- return {
590
- ...token,
591
- balance: holding.amount,
592
- marketValue: holding.marketValue,
593
- holding: holding,
594
- } as MatchedToken;
595
- }
596
- return null;
597
- })
598
- .filter(
599
- (token): token is MatchedToken =>
600
- token !== null && token.marketValue > 5,
601
- )
602
- .sort((a, b) => b.marketValue - a.marketValue);
603
-
604
- console.log("Matched tokens with balances:", matched);
605
- setMatchedTokens(matched);
606
- } else {
607
- throw new Error(
608
- tokensData.message ||
609
- "Failed to fetch supported tokens",
610
- );
611
- }
612
- } catch (err) {
613
- debugger;
614
- console.error("Error fetching data:", err);
615
- setError(
616
- err instanceof Error ? err.message : "Failed to fetch data",
493
+ const tokensResponse = await fetch(
494
+ `${CHECKOUT_BFF_URL}/tokens?chainId=${chainIdIn}&brokerType=${encodeURIComponent(selectedIntegration?.type || "")}`,
495
+ );
496
+ const tokensData = await tokensResponse.json();
497
+
498
+ if (
499
+ tokensData.status !== "success" ||
500
+ !tokensData.content?.tokens
501
+ ) {
502
+ throw new Error(
503
+ tokensData.message || "Failed to fetch supported tokens",
617
504
  );
618
- } finally {
619
- setLoading(false);
620
505
  }
621
- };
622
506
 
623
- if (meshAccessToken && sessionId && chainIdIn && selectedIntegration) {
624
- fetchData();
625
- }
626
- }, [address, chainIdIn, meshAccessToken, sessionId, selectedIntegration]);
507
+ const matched = tokensData.content.tokens
508
+ .map((token: SupportedToken) => {
509
+ const holding =
510
+ holdingsData.content.cryptocurrencyPositions.find(
511
+ (h: CryptocurrencyPosition) =>
512
+ h.symbol === token.symbol,
513
+ );
514
+ if (holding) {
515
+ return {
516
+ ...token,
517
+ balance: holding.amount,
518
+ marketValue: holding.marketValue,
519
+ holding: holding,
520
+ } as MatchedToken;
521
+ }
522
+ return null;
523
+ })
524
+ .filter(
525
+ (token): token is MatchedToken =>
526
+ token !== null && token.marketValue > 5,
527
+ )
528
+ .sort((a, b) => b.marketValue - a.marketValue);
529
+
530
+ console.log("Matched tokens with balances:", matched);
531
+ return matched;
532
+ },
533
+ enabled: Boolean(
534
+ meshAccessToken && sessionId && chainIdIn && selectedIntegration,
535
+ ),
536
+ retry: 0,
537
+ });
538
+
539
+ useEffect(() => {
540
+ if (!(error instanceof Error)) return;
541
+ const meshError = error as MeshRequestError;
542
+ if (meshError.code !== "invalidIntegrationToken") return;
543
+
544
+ console.log("Invalid integration token");
545
+ setStep(WithdrawalStep.CheckSessionKey);
546
+ setMeshAccessToken(null);
547
+ setSessionId(null);
548
+ sessionStorage.removeItem(`${deviceKey}:${selectedIntegration?.type}`);
549
+ }, [
550
+ error,
551
+ setStep,
552
+ setMeshAccessToken,
553
+ setSessionId,
554
+ deviceKey,
555
+ selectedIntegration?.type,
556
+ ]);
627
557
 
628
558
  const geckoTokens = useTokenFromListBySymbols(
629
559
  matchedTokens.map((token) => token.symbol),
@@ -639,7 +569,10 @@ const ChooseAssetStep = ({
639
569
  if (error)
640
570
  return (
641
571
  <Box p={5} color="red.500">
642
- Error: {error}
572
+ Error:{" "}
573
+ {error instanceof Error
574
+ ? error.message
575
+ : "Failed to fetch data"}
643
576
  </Box>
644
577
  );
645
578
 
@@ -668,19 +601,24 @@ const ChooseAssetStep = ({
668
601
  loading={false}
669
602
  selected={selectedTokenSymbol === token.symbol}
670
603
  onClick={() => {
604
+ const tokenAddress =
605
+ geckoTokens?.[index]?.address;
671
606
  setSelectedTokenSymbol(token.symbol);
672
- onTokenSelect(token);
673
- setTokenIn(geckoTokens?.[index]?.address);
607
+ onTokenSelect({
608
+ ...token,
609
+ tokenAddress,
610
+ });
611
+ setTokenIn(tokenAddress);
674
612
  }}
675
613
  />
676
614
  ))}
677
615
  </ListWrapper>
678
616
  </Box>
679
- {matchedTokens.length === 0 && (
617
+ {matchedTokens.length === 0 ? (
680
618
  <Box textAlign="center" color="fg.subtle" py={8}>
681
619
  No tokens with balances found for this chain
682
620
  </Box>
683
- )}
621
+ ) : null}
684
622
  {
685
623
  <Button
686
624
  disabled={!selectedTokenSymbol}
@@ -695,441 +633,6 @@ const ChooseAssetStep = ({
695
633
  );
696
634
  };
697
635
 
698
- const ChooseDelayedBalance = ({
699
- setStep,
700
- onTokenSelect,
701
- }: {
702
- setStep: (step: WithdrawalStep) => void;
703
- onTokenSelect: (token: MatchedToken) => void;
704
- }) => {
705
- const { chainIdIn, setTokenIn, setChainIdIn } = useAppStore();
706
- const [selectedToken, setSelectedToken] = useState<string | null>(null);
707
-
708
- // Get smart account balances
709
- const { holdingsList, total, isLoading } = useSmartAccountBalances(1);
710
- const setSelectedIntegration = useAppStore(
711
- (state) => state.setSelectedIntegration,
712
- );
713
-
714
- useEffect(() => {
715
- setSelectedIntegration({
716
- type: "delayed",
717
- name: "Smart account",
718
- id: "",
719
- });
720
- }, []);
721
-
722
- if (isLoading) {
723
- return (
724
- <Center>
725
- <Spinner m={5} />
726
- </Center>
727
- );
728
- }
729
-
730
- return (
731
- <BodyWrapper>
732
- <Box mb={4} width="100%" textAlign="left">
733
- <HeaderDescription>
734
- Smart Account Balance: {formatUSD(total)}
735
- </HeaderDescription>
736
- </Box>
737
- <Box overflowY={"scroll"} maxH={"400px"}>
738
- <ListWrapper>
739
- {holdingsList?.map((asset) => (
740
- <AssetCard
741
- key={`${asset.token}-${asset.chainId}`}
742
- chainId={asset.chainId}
743
- icon={asset.logoUri}
744
- title={asset.name}
745
- balance={`${formatNumber(
746
- normalizeValue(asset.amount, asset.decimals),
747
- )} ${asset.symbol}`}
748
- usdBalance={formatUSD(asset.total)}
749
- tag=""
750
- loading={false}
751
- selected={
752
- selectedToken === asset.token &&
753
- chainIdIn === asset.chainId
754
- }
755
- onClick={() => {
756
- setSelectedToken(asset.token);
757
- setTokenIn(asset.token);
758
- setChainIdIn(asset.chainId);
759
- // Mock MatchedToken from balance data
760
- const mockMatchedToken: MatchedToken = {
761
- symbol: asset.symbol,
762
- name: asset.name,
763
- networkId: asset.chainId.toString(),
764
- chainId: asset.chainId,
765
- integrationNetworks: [],
766
- balance: Number(
767
- normalizeValue(
768
- asset.amount,
769
- asset.decimals,
770
- ),
771
- ),
772
- marketValue: asset.total,
773
- };
774
- onTokenSelect(mockMatchedToken);
775
- }}
776
- />
777
- ))}
778
- </ListWrapper>
779
- </Box>
780
- {holdingsList?.length === 0 && (
781
- <Box textAlign="center" color="fg.subtle" py={8}>
782
- No tokens found in smart account
783
- </Box>
784
- )}
785
- <Button
786
- disabled={!selectedToken}
787
- onClick={() => {
788
- setStep(WithdrawalStep.ChooseAmount);
789
- }}
790
- >
791
- Continue
792
- </Button>
793
- </BodyWrapper>
794
- );
795
- };
796
-
797
- const ChooseAmountStep = ({
798
- setStep,
799
- selectedToken,
800
- }: {
801
- setStep: (step: WithdrawalStep) => void;
802
- selectedToken: MatchedToken | null;
803
- }) => {
804
- const [amountInput, setAmountInput] = useState<AmountInputValue>({
805
- tokenAmount: "",
806
- usdAmount: "",
807
- mode: "usd",
808
- });
809
- const setAmountIn = useAppStore((s) => s.setAmountIn);
810
- const { tokenInData, selectedIntegration } = useAppDetails();
811
- const isStable = selectedToken?.symbol.toLowerCase().includes("USD");
812
- const roundingPrecision = isStable ? 2 : 6;
813
- const amount = amountInput.tokenAmount;
814
- const usdValue = amountInput.usdAmount;
815
-
816
- // Only apply CEX withdrawal limits if using a CEX holding (not smart account)
817
- const isWithdrawal = !isDelayedBalanceUsed(selectedIntegration.type);
818
-
819
- const maxUsdAmount = selectedToken
820
- ? isWithdrawal
821
- ? (selectedToken.marketValue - EXCHANGE_MAX_LIMIT_GAP_USD).toFixed(
822
- 2,
823
- )
824
- : selectedToken.marketValue.toFixed(2)
825
- : 0;
826
-
827
- const tokenPriceUsd = useMemo(() => {
828
- if (!selectedToken || !selectedToken.balance) return undefined;
829
- return selectedToken.marketValue / selectedToken.balance;
830
- }, [selectedToken]);
831
-
832
- // Handle percentage selection with limits (only for CEX withdrawals)
833
- const getPercentAmounts = useCallback(
834
- (percent: number) => {
835
- if (!selectedToken) return;
836
-
837
- // Calculate target USD amount based on percentage
838
- const targetUsdAmount = (selectedToken.marketValue * percent) / 100;
839
-
840
- let finalUsdAmount: number;
841
- let finalTokenAmount: number;
842
-
843
- if (isWithdrawal) {
844
- // Apply limits for CEX withdrawals
845
- const minValueForToken =
846
- EXCHANGE_MIN_LIMIT[
847
- selectedToken.symbol as keyof typeof EXCHANGE_MIN_LIMIT
848
- ] || 0;
849
-
850
- finalUsdAmount = Math.max(
851
- minValueForToken,
852
- Math.min(targetUsdAmount, +maxUsdAmount),
853
- );
854
-
855
- const tokenPrice =
856
- selectedToken.marketValue / selectedToken.balance;
857
- finalTokenAmount = Math.min(
858
- finalUsdAmount / tokenPrice,
859
- selectedToken.balance,
860
- );
861
- } else {
862
- // No limits for smart account balances
863
- finalUsdAmount = targetUsdAmount;
864
- finalTokenAmount = (selectedToken.balance * percent) / 100;
865
- }
866
-
867
- return {
868
- tokenAmount: precisionizeNumber(
869
- finalTokenAmount,
870
- roundingPrecision,
871
- ),
872
- usdAmount: finalUsdAmount.toFixed(2),
873
- };
874
- },
875
- [selectedToken, isWithdrawal, maxUsdAmount, roundingPrecision],
876
- );
877
-
878
- // Set max value on load
879
- useEffect(() => {
880
- if (selectedToken) {
881
- const percentAmounts = getPercentAmounts(100);
882
- if (!percentAmounts) return;
883
-
884
- setAmountInput((prev) => ({
885
- ...prev,
886
- ...percentAmounts,
887
- }));
888
- }
889
- }, [selectedToken, getPercentAmounts]);
890
-
891
- useEffect(() => {
892
- if (!tokenInData?.decimals) return;
893
-
894
- const normalizedAmount = amount.endsWith(".")
895
- ? amount.slice(0, -1)
896
- : amount;
897
-
898
- if (!normalizedAmount || normalizedAmount === ".") {
899
- setAmountIn("0");
900
- return;
901
- }
902
-
903
- try {
904
- setAmountIn(denormalizeValue(normalizedAmount, tokenInData.decimals));
905
- } catch (error) {
906
- setAmountIn("0");
907
- }
908
- }, [amount, tokenInData?.decimals, setAmountIn]);
909
-
910
- const numericAmount = getPositiveDecimalValue(amount);
911
- const hasPositiveAmount = numericAmount !== null;
912
- const hasUsdValue = !!usdValue && usdValue !== ".";
913
- const notEnoughBalance = selectedToken
914
- ? hasPositiveAmount &&
915
- numericAmount !== null &&
916
- numericAmount > selectedToken.balance
917
- : true;
918
-
919
- // Limits validation logic - only for CEX withdrawals
920
- const currentUsdValue = hasUsdValue
921
- ? getPositiveDecimalValue(usdValue) ?? 0
922
- : 0;
923
- const minValueForToken =
924
- isWithdrawal && selectedToken
925
- ? EXCHANGE_MIN_LIMIT[
926
- selectedToken.symbol as keyof typeof EXCHANGE_MIN_LIMIT
927
- ]
928
- : 0;
929
-
930
- const isBelowMinAmount =
931
- isWithdrawal &&
932
- selectedToken &&
933
- hasPositiveAmount &&
934
- currentUsdValue > 0 &&
935
- minValueForToken &&
936
- numericAmount !== null &&
937
- numericAmount < minValueForToken;
938
- const isAboveMaxAmount =
939
- isWithdrawal &&
940
- selectedToken &&
941
- hasPositiveAmount &&
942
- currentUsdValue > 0 &&
943
- currentUsdValue > +maxUsdAmount;
944
-
945
- const isAmountInvalid =
946
- !hasPositiveAmount ||
947
- isBelowMinAmount ||
948
- isAboveMaxAmount ||
949
- notEnoughBalance;
950
-
951
- if (!selectedToken) {
952
- return (
953
- <BodyWrapper>
954
- <Box textAlign="center" color="fg.subtle" py={8}>
955
- No token selected
956
- </Box>
957
- </BodyWrapper>
958
- );
959
- }
960
-
961
- return (
962
- <BodyWrapper>
963
- <Box mb={4} width="100%" textAlign="left">
964
- <HeaderTitle>Enter Amount</HeaderTitle>
965
- <HeaderDescription>
966
- Available: {formatNumber(selectedToken.balance)}{" "}
967
- {selectedToken.symbol} (
968
- {formatUSD(selectedToken.marketValue)})
969
- </HeaderDescription>
970
- </Box>
971
-
972
- <Box
973
- display={"flex"}
974
- flexDirection={"column"}
975
- gap={"8px"}
976
- width="100%"
977
- >
978
- <AmountInput
979
- value={amountInput}
980
- onChange={setAmountInput}
981
- tokenSymbol={selectedToken.symbol}
982
- tokenPriceUsd={tokenPriceUsd}
983
- roundingPrecision={roundingPrecision}
984
- onPercentSelect={getPercentAmounts}
985
- />
986
- </Box>
987
-
988
- {
989
- <Box
990
- textAlign="center"
991
- color="fg.subtle"
992
- fontSize="xs"
993
- h={3}
994
- m={-1}
995
- visibility={
996
- isAmountInvalid ? "visible" : "hidden"
997
- }
998
- >
999
- {!hasPositiveAmount
1000
- ? "Please enter an amount"
1001
- : isBelowMinAmount
1002
- ? `Minimum amount is ${formatNumber(minValueForToken)} ${selectedToken.symbol}`
1003
- : isAboveMaxAmount
1004
- ? `Maximum amount is ${formatUSD(maxUsdAmount)} (balance - $${EXCHANGE_MAX_LIMIT_GAP_USD})`
1005
- : notEnoughBalance
1006
- ? "Amount exceeds available balance"
1007
- : ""}
1008
- </Box>
1009
- }
1010
-
1011
- <Button
1012
- onClick={() =>
1013
- isAmountInvalid
1014
- ? undefined
1015
- : setStep(WithdrawalStep.SignUserOp)
1016
- }
1017
- disabled={isAmountInvalid}
1018
- >
1019
- Continue
1020
- </Button>
1021
- </BodyWrapper>
1022
- );
1023
- };
1024
-
1025
- const SignUserOpStep = ({
1026
- setStep,
1027
- setUserOp,
1028
- nextStep,
1029
- }: {
1030
- nextStep: WithdrawalStep;
1031
- setStep: (step: WithdrawalStep) => void;
1032
- setUserOp: (userOp: any) => void;
1033
- }) => {
1034
- const { chainIdIn } = useAppDetails();
1035
- const { routeLoading, usdAmountIn, routeData } = useRouteData();
1036
- const { signMessageAsync } = useSignMessage();
1037
- const { address } = useAccount();
1038
- const [isSigning, setIsSigning] = useState(false);
1039
-
1040
- const handleSignUserOp = async () => {
1041
- if (!routeData || (routeData as any)?.error) {
1042
- console.error("No valid router data available");
1043
- return;
1044
- }
1045
-
1046
- try {
1047
- setIsSigning(true);
1048
-
1049
- // Extract userOp from routeData
1050
- const userOperation = routeData?.userOp;
1051
- if (!userOperation) {
1052
- console.error("No userOperation found in routeData");
1053
- return;
1054
- }
1055
-
1056
- console.log("Signing userOperation:", userOperation);
1057
-
1058
- // Use viem's getUserOperationHash function
1059
- const userOpHash = getUserOperationHash({
1060
- // @ts-ignore
1061
- userOperation,
1062
- entryPointAddress: ENTRY_POINT_ADDRESS,
1063
- entryPointVersion: "0.7",
1064
- chainId: chainIdIn,
1065
- });
1066
-
1067
- // Sign the userOperation hash directly
1068
- const signature = await signMessageAsync({
1069
- account: address as `0x${string}`,
1070
- message: { raw: userOpHash as `0x${string}` },
1071
- });
1072
-
1073
- // Update userOperation with signature
1074
- const signedUserOp = {
1075
- ...userOperation,
1076
- signature,
1077
- };
1078
-
1079
- console.log("signedUserOp", JSON.stringify(signedUserOp));
1080
-
1081
- setUserOp(signedUserOp);
1082
- setStep(nextStep);
1083
- } catch (error) {
1084
- console.error("Failed to sign userOperation:", error);
1085
- } finally {
1086
- setIsSigning(false);
1087
- }
1088
- };
1089
-
1090
- return (
1091
- <BodyWrapper>
1092
- <Flex
1093
- flexDirection={"column"}
1094
- gap={"16px"}
1095
- alignItems={"center"}
1096
- width={"100%"}
1097
- >
1098
- <Skeleton
1099
- loading={routeLoading}
1100
- width={routeLoading ? "156px" : "auto"}
1101
- >
1102
- <Input
1103
- readOnly
1104
- marginY={"8px"}
1105
- variant={"text"}
1106
- placeholder={"$10.00"}
1107
- value={usdAmountIn}
1108
- />
1109
- </Skeleton>
1110
-
1111
- <QuoteParameters />
1112
- </Flex>
1113
-
1114
- <TransactionDetailRow />
1115
-
1116
- <Button
1117
- disabled={
1118
- !!(routeData as any)?.message || routeLoading || isSigning
1119
- }
1120
- loading={routeLoading || isSigning}
1121
- onClick={handleSignUserOp}
1122
- >
1123
- {routeLoading
1124
- ? "Loading quote"
1125
- : isSigning
1126
- ? "Signing..."
1127
- : "Sign Transaction"}
1128
- </Button>
1129
- </BodyWrapper>
1130
- );
1131
- };
1132
-
1133
636
  const InitiateWithdrawalStep = ({
1134
637
  selectedToken,
1135
638
  userOp,
@@ -1156,7 +659,6 @@ const InitiateWithdrawalStep = ({
1156
659
  return;
1157
660
  }
1158
661
 
1159
- // Convert amountIn from wei to token amount
1160
662
  const transferAmount = tokenInData?.decimals
1161
663
  ? normalizeValue(amountIn, tokenInData.decimals)
1162
664
  : 0;
@@ -1173,19 +675,16 @@ const InitiateWithdrawalStep = ({
1173
675
  amount: transferAmount,
1174
676
  },
1175
677
  ];
1176
- const meshData = {
678
+
679
+ console.log("link request body", {
1177
680
  restrictMultipleAccounts: true,
1178
681
  userId: deviceKey,
1179
682
  integrationId: selectedIntegration?.id,
1180
- transferOptions: {
1181
- toAddresses,
1182
- },
1183
- };
1184
-
1185
- console.log("link request body", meshData);
683
+ transferOptions: { toAddresses },
684
+ });
1186
685
 
1187
686
  const response = await fetch(
1188
- `${MESH_API_URL}/linktoken?brokerType=${encodeURIComponent(selectedIntegration?.type || "")}`,
687
+ `${CHECKOUT_BFF_URL}/linktoken?brokerType=${encodeURIComponent(selectedIntegration?.type || "")}`,
1189
688
  {
1190
689
  method: "POST",
1191
690
  headers: {
@@ -1222,7 +721,7 @@ const InitiateWithdrawalStep = ({
1222
721
  clientId: address,
1223
722
  accessTokens,
1224
723
  onIntegrationConnected: (payload) => {
1225
- handleMeshAccessPayload(payload.accessToken, sessionId); // Persist access token and session id for future reloads
724
+ handleMeshAccessPayload(payload.accessToken, sessionId);
1226
725
  console.log("Integration connected", payload);
1227
726
  },
1228
727
  onTransferFinished: (transferData) => {
@@ -1270,7 +769,14 @@ const InitiateWithdrawalStep = ({
1270
769
  if (error) {
1271
770
  return (
1272
771
  <BodyWrapper>
1273
- <Flex direction="column" align="center" justify="center" flex={1} p={6} gap={4}>
772
+ <Flex
773
+ direction="column"
774
+ align="center"
775
+ justify="center"
776
+ flex={1}
777
+ p={6}
778
+ gap={4}
779
+ >
1274
780
  <Flex
1275
781
  align="center"
1276
782
  justify="center"
@@ -1279,7 +785,11 @@ const InitiateWithdrawalStep = ({
1279
785
  borderRadius="full"
1280
786
  bg="orange.100"
1281
787
  >
1282
- <Icon as={TriangleAlert} boxSize={6} color="orange.500" />
788
+ <Icon
789
+ as={TriangleAlert}
790
+ boxSize={6}
791
+ color="orange.500"
792
+ />
1283
793
  </Flex>
1284
794
  <Text fontSize="md" textAlign="center" color="gray.700">
1285
795
  {error}
@@ -1307,657 +817,24 @@ const InitiateWithdrawalStep = ({
1307
817
  );
1308
818
  };
1309
819
 
1310
- // Phase indicator component for cross-chain tracking
1311
- const PhaseIndicator = ({
1312
- currentPhase,
1313
- phases,
1314
- }: {
1315
- currentPhase: number;
1316
- phases: string[];
1317
- }) => {
1318
- return (
1319
- <Box display="flex" gap={4} justifyContent="center" mb={4}>
1320
- {phases.map((phase, index) => (
1321
- <Box key={phase} display="flex" alignItems="center" gap={2}>
1322
- <Box
1323
- w={6}
1324
- h={6}
1325
- borderRadius="full"
1326
- bg={
1327
- index < currentPhase
1328
- ? "green.500"
1329
- : index === currentPhase
1330
- ? "blue.500"
1331
- : "gray.300"
1332
- }
1333
- display="flex"
1334
- alignItems="center"
1335
- justifyContent="center"
1336
- color="white"
1337
- fontSize="xs"
1338
- fontWeight="bold"
1339
- >
1340
- {index < currentPhase ? "✓" : index + 1}
1341
- </Box>
1342
- <Text
1343
- fontSize="sm"
1344
- color={index === currentPhase ? "fg" : "fg.muted"}
1345
- fontWeight={
1346
- index === currentPhase ? "semibold" : "normal"
1347
- }
1348
- >
1349
- {phase}
1350
- </Text>
1351
- </Box>
1352
- ))}
1353
- </Box>
1354
- );
1355
- };
1356
-
1357
- const TrackUserOpStep = ({
1358
- selectedToken,
1359
- userOp,
1360
- setStep,
1361
- }: {
1362
- selectedToken: MatchedToken | null;
1363
- userOp: any;
1364
- setStep: (step: WithdrawalStep) => void;
1365
- }) => {
1366
- const { chainIdIn, chainIdOut, tokenInData } = useAppDetails();
1367
- const { amountIn } = useAppStore();
1368
-
1369
- // Determine if this is a cross-chain (bridge) flow
1370
- const isCrosschain = chainIdIn !== chainIdOut;
1371
-
1372
- // Phase management: for crosschain 'cex' -> 'bridge' -> 'completed', for single-chain just 'cex' -> 'completed'
1373
- const [phase, setPhase] = useState<
1374
- "cex" | "bridge" | "completed" | "failed"
1375
- >("cex");
1376
- const [operationId, setOperationId] = useState<string | null>(null);
1377
- const [status, setStatus] = useState<
1378
- "sending" | "tracking" | "completed" | "failed"
1379
- >("sending");
1380
- const [message, setMessage] = useState("Sending operation to tracker...");
1381
- const [txHash, setTxHash] = useState<`0x${string}` | null>(null);
1382
- const [isTimerFinished, setIsTimerFinished] = useState(false);
1383
- const [trackingInterval, setTrackingInterval] =
1384
- useState<NodeJS.Timeout | null>(null);
1385
- const [destinationVerified, setDestinationVerified] = useState(false);
1386
- const [destinationSuccess, setDestinationSuccess] = useState<
1387
- boolean | null
1388
- >(null);
1389
- const [refundDetails, setRefundDetails] = useState<{
1390
- token: string;
1391
- amount: string;
1392
- recipient: string;
1393
- isNative: boolean;
1394
- } | null>(null);
1395
- const [destinationTxHash, setDestinationTxHash] = useState<string | null>(
1396
- null,
1397
- );
1398
-
1399
- // LayerZero tracking for bridge progress (real-time updates)
1400
- const lzStatus = useLayerZeroStatus(
1401
- txHash ?? undefined,
1402
- isCrosschain && phase === "bridge",
1403
- );
1404
-
1405
- // Send UserOp to tracker
1406
- useEffect(() => {
1407
- const sendUserOpToTracker = async () => {
1408
- if (!selectedToken || !userOp || !tokenInData) {
1409
- console.error("Missing required data for tracking");
1410
- setStatus("failed");
1411
- setMessage("Missing required data");
1412
- setPhase("failed");
1413
- return;
1414
- }
1415
-
1416
- try {
1417
- const response = await fetch(
1418
- "https://alpha-scanners-dev-054573dc8549.herokuapp.com/operations",
1419
- {
1420
- method: "POST",
1421
- headers: {
1422
- "Content-Type": "application/json",
1423
- },
1424
- body: JSON.stringify({
1425
- userOperationData: {
1426
- sender: userOp.sender,
1427
- nonce: userOp.nonce,
1428
- factory: userOp.factory,
1429
- factoryData: userOp.factoryData,
1430
- callData: userOp.callData,
1431
- callGasLimit: userOp.callGasLimit,
1432
- verificationGasLimit:
1433
- userOp.verificationGasLimit,
1434
- preVerificationGas: userOp.preVerificationGas,
1435
- maxFeePerGas: userOp.maxFeePerGas,
1436
- maxPriorityFeePerGas:
1437
- userOp.maxPriorityFeePerGas,
1438
- paymaster: userOp.paymaster,
1439
- paymasterData: userOp.paymasterData,
1440
- paymasterVerificationGasLimit:
1441
- userOp.paymasterVerificationGasLimit,
1442
- paymasterPostOpGasLimit:
1443
- userOp.paymasterPostOpGasLimit,
1444
- signature: userOp.signature,
1445
- },
1446
- chainId: chainIdIn,
1447
- expectedBalance: amountIn,
1448
- tokenAddress: tokenInData.address,
1449
- }),
1450
- },
1451
- );
1452
-
1453
- const data = await response.json();
1454
- console.log("Operation tracking response:", data);
1455
-
1456
- if (data.success && data.operationId) {
1457
- setOperationId(data.operationId);
1458
- setStatus("tracking");
1459
- setMessage(
1460
- isCrosschain
1461
- ? "Funds forwarding in progress..."
1462
- : "Tracking operation progress...",
1463
- );
1464
- } else {
1465
- throw new Error(
1466
- data.message || "Failed to send operation to tracker",
1467
- );
1468
- }
1469
- } catch (error) {
1470
- console.error("Failed to send operation to tracker:", error);
1471
- setStatus("failed");
1472
- setMessage("Failed to send operation to tracker");
1473
- setPhase("failed");
1474
- }
1475
- };
1476
-
1477
- sendUserOpToTracker();
1478
- }, []);
1479
-
1480
- // Track operation status
1481
- useEffect(() => {
1482
- if (!operationId || status !== "tracking") return;
1483
-
1484
- const trackOperation = async () => {
1485
- try {
1486
- const response = await fetch(
1487
- `https://alpha-scanners-dev-054573dc8549.herokuapp.com/operations/${operationId}/status`,
1488
- );
1489
- const data = await response.json();
1490
- console.log("Operation status:", data);
1491
-
1492
- if (data.operation?.status === "completed") {
1493
- setStatus("completed");
1494
-
1495
- if (isCrosschain) {
1496
- // For crosschain: move to bridge phase
1497
- setMessage("Funds forwarding completed!");
1498
- if (data.operation?.bundleTxHash) {
1499
- setTxHash(
1500
- data.operation.bundleTxHash as `0x${string}`,
1501
- );
1502
- setPhase("bridge");
1503
- } else {
1504
- console.warn(
1505
- "No bundleTxHash returned from indexer, cannot track bridge",
1506
- );
1507
- setPhase("completed");
1508
- }
1509
- } else {
1510
- // For single-chain: complete
1511
- setMessage("Operation completed successfully!");
1512
- setPhase("completed");
1513
- }
1514
-
1515
- if (trackingInterval) {
1516
- clearInterval(trackingInterval);
1517
- setTrackingInterval(null);
1518
- }
1519
- } else if (
1520
- ["failed", "failed_permanent"].includes(
1521
- data.operation?.status,
1522
- )
1523
- ) {
1524
- setStatus("failed");
1525
- setMessage(
1526
- isCrosschain
1527
- ? "Bridging failed. Please select Smart-account balance as a source to use withdrawn funds"
1528
- : "Operation failed",
1529
- );
1530
- setPhase("failed");
1531
- if (trackingInterval) {
1532
- clearInterval(trackingInterval);
1533
- setTrackingInterval(null);
1534
- }
1535
- }
1536
- } catch (error) {
1537
- console.error("Failed to fetch operation status:", error);
1538
- }
1539
- };
1540
-
1541
- const interval = setInterval(trackOperation, 3000);
1542
- setTrackingInterval(interval);
1543
-
1544
- return () => {
1545
- if (interval) clearInterval(interval);
1546
- };
1547
- }, [operationId, status, isCrosschain]);
1548
-
1549
- // Handle bridge completion - verify destination execution once LayerZero shows DELIVERED
1550
- useEffect(() => {
1551
- if (!isCrosschain || phase !== "bridge") return;
1552
-
1553
- // If LayerZero failed, mark as failed immediately
1554
- if (lzStatus.isFailed) {
1555
- setPhase("failed");
1556
- return;
1557
- }
1558
-
1559
- // When LayerZero shows DELIVERED, verify destination execution with Enso API
1560
- if (
1561
- lzStatus.isComplete &&
1562
- !destinationVerified &&
1563
- txHash &&
1564
- chainIdIn
1565
- ) {
1566
- // TODO: use hook
1567
- const verifyDestination = async () => {
1568
- try {
1569
- const res = await fetch(
1570
- `https://api.enso.build/api/v1/layerzero/bridge/check?chainId=${chainIdIn}&txHash=${txHash}`,
1571
- );
1572
- if (!res.ok) {
1573
- // If API call fails, assume success (LayerZero delivered)
1574
- setDestinationVerified(true);
1575
- setDestinationSuccess(true);
1576
- setPhase("completed");
1577
- return;
1578
- }
1579
- const data = await res.json();
1580
- setDestinationVerified(true);
1581
- setDestinationTxHash(data.destinationTxHash || null);
1582
-
1583
- if (data.status === "success") {
1584
- setDestinationSuccess(true);
1585
- setPhase("completed");
1586
- } else if (data.status === "failed") {
1587
- setDestinationSuccess(false);
1588
- setRefundDetails(
1589
- data.ensoDestinationEvent?.refundDetails || null,
1590
- );
1591
- setPhase("failed");
1592
- } else {
1593
- // Still pending, assume success since LZ delivered
1594
- setDestinationSuccess(true);
1595
- setPhase("completed");
1596
- }
1597
- } catch (error) {
1598
- console.error("Failed to verify destination:", error);
1599
- // On error, assume success since LayerZero delivered
1600
- setDestinationVerified(true);
1601
- setDestinationSuccess(true);
1602
- setPhase("completed");
1603
- }
1604
- };
1605
- verifyDestination();
1606
- }
1607
- }, [
1608
- phase,
1609
- lzStatus.isComplete,
1610
- lzStatus.isFailed,
1611
- isCrosschain,
1612
- destinationVerified,
1613
- txHash,
1614
- chainIdIn,
1615
- ]);
1616
-
1617
- const handleTimerFinish = () => {
1618
- setIsTimerFinished(true);
1619
- };
1620
-
1621
- const getOverallStatus = () => {
1622
- if (phase === "failed") return "failed";
1623
- if (phase === "completed") return "completed";
1624
- return "processing";
1625
- };
1626
-
1627
- const getStatusColor = () => {
1628
- switch (getOverallStatus()) {
1629
- case "completed":
1630
- return "#14AE5C";
1631
- case "failed":
1632
- return "#E84142";
1633
- default:
1634
- return "#1E171F";
1635
- }
1636
- };
1637
-
1638
- const getStatusText = () => {
1639
- switch (getOverallStatus()) {
1640
- case "completed":
1641
- return "Success";
1642
- case "failed":
1643
- return "Failed";
1644
- default:
1645
- return "Processing";
1646
- }
1647
- };
1648
-
1649
- const getCurrentMessage = () => {
1650
- if (isCrosschain) {
1651
- if (phase === "cex") {
1652
- return `(1/2) ${message}`;
1653
- } else if (phase === "bridge") {
1654
- return `(2/2) ${lzStatus.message}`;
1655
- } else if (phase === "completed") {
1656
- return "Transfer completed successfully!";
1657
- } else {
1658
- return refundDetails
1659
- ? "Destination execution failed. Funds refunded to smart account."
1660
- : "Transfer failed";
1661
- }
1662
- } else {
1663
- if (phase === "completed") {
1664
- return "Operation completed successfully!";
1665
- } else if (phase === "failed") {
1666
- return "Operation failed";
1667
- }
1668
- return message;
1669
- }
1670
- };
1671
-
1672
- const renderStatusIcon = () => {
1673
- const isProcessing = phase === "cex" || phase === "bridge";
1674
-
1675
- if (isProcessing) {
1676
- return (
1677
- <CircleTimer
1678
- start={status === "tracking" || phase === "bridge"}
1679
- onFinish={handleTimerFinish}
1680
- duration={isCrosschain ? 180 : 120}
1681
- />
1682
- );
1683
- }
1684
-
1685
- if (phase === "completed") {
1686
- return (
1687
- <Box display="flex" flexDirection="column" alignItems="center">
1688
- <Image
1689
- src={SuccessIcon}
1690
- boxShadow="0px 0px 20px #14AE5C"
1691
- borderRadius="90%"
1692
- width="58px"
1693
- height="58px"
1694
- />
1695
- </Box>
1696
- );
1697
- }
1698
-
1699
- return (
1700
- <Box display="flex" flexDirection="column" alignItems="center">
1701
- <Image
1702
- src={FailIcon}
1703
- boxShadow="0px 0px 20px #E84142"
1704
- borderRadius="90%"
1705
- width="58px"
1706
- height="58px"
1707
- />
1708
- </Box>
1709
- );
1710
- };
1711
-
1712
- const intermediateChainName = chainIdIn
1713
- ? STARGATE_CHAIN_NAMES[chainIdIn as keyof typeof STARGATE_CHAIN_NAMES]
1714
- : "Unknown";
1715
- const targetChainName = chainIdOut
1716
- ? STARGATE_CHAIN_NAMES[chainIdOut as keyof typeof STARGATE_CHAIN_NAMES]
1717
- : "Unknown";
1718
-
1719
- return (
1720
- <BodyWrapper>
1721
- {/* Phase Indicator (crosschain only) */}
1722
- {isCrosschain && (
1723
- <PhaseIndicator
1724
- currentPhase={
1725
- phase === "cex" ? 0 : phase === "bridge" ? 1 : 2
1726
- }
1727
- phases={["Forward funds", "Bridge"]}
1728
- />
1729
- )}
1730
-
1731
- {/* Status Icon */}
1732
- <Box
1733
- display="flex"
1734
- flexDirection="column"
1735
- paddingBottom="16px"
1736
- alignItems="center"
1737
- width="100%"
1738
- >
1739
- {renderStatusIcon()}
1740
- <Box
1741
- display="flex"
1742
- flexDirection="column"
1743
- alignItems="center"
1744
- marginTop="16px"
1745
- textAlign="center"
1746
- >
1747
- <Text
1748
- fontSize="lg"
1749
- fontWeight="semibold"
1750
- color="fg"
1751
- marginBottom="8px"
1752
- >
1753
- {getCurrentMessage()}
1754
- </Text>
1755
- {(phase === "cex" || phase === "bridge") &&
1756
- isTimerFinished && (
1757
- <Text
1758
- fontSize="sm"
1759
- color="fg.muted"
1760
- maxWidth="280px"
1761
- >
1762
- Your operation is being processed – no action is
1763
- required from you.
1764
- </Text>
1765
- )}
1766
- </Box>
1767
- </Box>
1768
-
1769
- {/* Status Table */}
1770
- <Table.Root key="status" size="sm" variant="outline" width="100%">
1771
- <Table.Body>
1772
- <Table.Row>
1773
- <Table.Cell>Status</Table.Cell>
1774
- <Table.Cell
1775
- display="flex"
1776
- textAlign="end"
1777
- justifyContent="end"
1778
- >
1779
- <Text color={getStatusColor()}>
1780
- {getStatusText()}
1781
- </Text>
1782
- </Table.Cell>
1783
- </Table.Row>
1784
- {isCrosschain && (
1785
- <>
1786
- <Table.Row>
1787
- <Table.Cell>Current Phase</Table.Cell>
1788
- <Table.Cell
1789
- display="flex"
1790
- textAlign="end"
1791
- justifyContent="end"
1792
- >
1793
- <Text>
1794
- {phase === "cex"
1795
- ? "Awaiting funds"
1796
- : phase === "bridge"
1797
- ? "Bridging"
1798
- : phase === "completed"
1799
- ? "Complete"
1800
- : "Failed"}
1801
- </Text>
1802
- </Table.Cell>
1803
- </Table.Row>
1804
- <Table.Row>
1805
- <Table.Cell>Intermediate Chain</Table.Cell>
1806
- <Table.Cell
1807
- display="flex"
1808
- textAlign="end"
1809
- justifyContent="end"
1810
- >
1811
- <Text textTransform="capitalize">
1812
- {intermediateChainName}
1813
- </Text>
1814
- </Table.Cell>
1815
- </Table.Row>
1816
- <Table.Row>
1817
- <Table.Cell>Final Destination</Table.Cell>
1818
- <Table.Cell
1819
- display="flex"
1820
- textAlign="end"
1821
- justifyContent="end"
1822
- >
1823
- <Text textTransform="capitalize">
1824
- {targetChainName}
1825
- </Text>
1826
- </Table.Cell>
1827
- </Table.Row>
1828
- </>
1829
- )}
1830
- {operationId && (
1831
- <Table.Row>
1832
- <Table.Cell>Operation ID</Table.Cell>
1833
- <Table.Cell
1834
- display="flex"
1835
- textAlign="end"
1836
- justifyContent="end"
1837
- >
1838
- <Text fontSize="sm" color="fg.muted">
1839
- {operationId}
1840
- </Text>
1841
- </Table.Cell>
1842
- </Table.Row>
1843
- )}
1844
- {isCrosschain && txHash && (
1845
- <Table.Row>
1846
- <Table.Cell>Bridge TX</Table.Cell>
1847
- <Table.Cell
1848
- display="flex"
1849
- textAlign="end"
1850
- justifyContent="end"
1851
- >
1852
- <Text
1853
- fontSize="sm"
1854
- color="blue.500"
1855
- cursor="pointer"
1856
- onClick={() =>
1857
- window.open(
1858
- `https://layerzeroscan.com/tx/${txHash}`,
1859
- "_blank",
1860
- )
1861
- }
1862
- >
1863
- View on LayerZero
1864
- </Text>
1865
- </Table.Cell>
1866
- </Table.Row>
1867
- )}
1868
- {isCrosschain && destinationTxHash && (
1869
- <Table.Row>
1870
- <Table.Cell>Destination TX</Table.Cell>
1871
- <Table.Cell
1872
- display="flex"
1873
- textAlign="end"
1874
- justifyContent="end"
1875
- >
1876
- <Text
1877
- fontSize="sm"
1878
- color="blue.500"
1879
- cursor="pointer"
1880
- onClick={() => {
1881
- const explorer =
1882
- CHAINS_ETHERSCAN[
1883
- chainIdOut as keyof typeof CHAINS_ETHERSCAN
1884
- ] || "https://etherscan.io";
1885
- window.open(
1886
- `${explorer}/tx/${destinationTxHash}`,
1887
- "_blank",
1888
- );
1889
- }}
1890
- >
1891
- View on Explorer
1892
- </Text>
1893
- </Table.Cell>
1894
- </Table.Row>
1895
- )}
1896
- {isCrosschain && phase === "bridge" && (
1897
- <Table.Row>
1898
- <Table.Cell>Bridge Progress</Table.Cell>
1899
- <Table.Cell
1900
- display="flex"
1901
- textAlign="end"
1902
- justifyContent="end"
1903
- >
1904
- <Text>({lzStatus.step}/4)</Text>
1905
- </Table.Cell>
1906
- </Table.Row>
1907
- )}
1908
- {isCrosschain && refundDetails && (
1909
- <Table.Row>
1910
- <Table.Cell>Refund</Table.Cell>
1911
- <Table.Cell
1912
- display="flex"
1913
- textAlign="end"
1914
- justifyContent="end"
1915
- >
1916
- <Text fontSize="sm" color="orange.500">
1917
- Funds refunded to smart account
1918
- </Text>
1919
- </Table.Cell>
1920
- </Table.Row>
1921
- )}
1922
- </Table.Body>
1923
- </Table.Root>
1924
-
1925
- <QuoteParameters />
1926
-
1927
- <TransactionDetailRow />
1928
-
1929
- {(phase === "completed" || phase === "failed") && (
1930
- <Button
1931
- onClick={() => setStep(WithdrawalStep.CheckSessionKey)}
1932
- visual="solid"
1933
- >
1934
- {phase === "completed" ? "New Deposit" : "Retry Deposit"}
1935
- </Button>
1936
- )}
1937
- </BodyWrapper>
1938
- );
1939
- };
1940
-
1941
820
  const ExchangeFlow = ({
1942
821
  setFlow,
1943
- initialStep = WithdrawalStep.ChooseExchange,
1944
822
  }: {
1945
- setFlow: (string) => void;
1946
- initialStep?: WithdrawalStep;
823
+ setFlow: (flow: string) => void;
1947
824
  }) => {
1948
825
  const { handleClose, enforceFlow } = useContext(CheckoutContext);
1949
- const [currentStep, setCurrentStep] = useState(initialStep);
826
+ const [currentStep, setCurrentStep] = useState<WithdrawalStep>(
827
+ WithdrawalStep.ChooseExchange,
828
+ );
1950
829
  const [selectedToken, setSelectedToken] = useState<MatchedToken | null>(
1951
830
  null,
1952
831
  );
1953
832
  const [userOp, setUserOp] = useState<any | null>(null);
1954
- const setSelectedIntegration = useAppStore(
1955
- (state) => state.setSelectedIntegration,
1956
- );
833
+
1957
834
  const selectedIntegration = useAppStore((s) => s.selectedIntegration);
1958
835
 
1959
836
  useEffect(() => {
1960
- return () => setSelectedIntegration(null);
837
+ return () => useAppStore.getState().setSelectedIntegration(null);
1961
838
  }, []);
1962
839
 
1963
840
  const currentStepComponent = (() => {
@@ -1973,29 +850,19 @@ const ExchangeFlow = ({
1973
850
  onTokenSelect={setSelectedToken}
1974
851
  />
1975
852
  );
1976
- case WithdrawalStep.ChooseBalanceAsset:
1977
- return (
1978
- <ChooseDelayedBalance
1979
- setStep={setCurrentStep}
1980
- onTokenSelect={setSelectedToken}
1981
- />
1982
- );
1983
853
  case WithdrawalStep.ChooseAmount:
1984
854
  return (
1985
- <ChooseAmountStep
855
+ <SharedChooseAmountStep
1986
856
  setStep={setCurrentStep}
857
+ nextStep={WithdrawalStep.SignUserOp}
1987
858
  selectedToken={selectedToken}
859
+ mode="cex"
1988
860
  />
1989
861
  );
1990
862
  case WithdrawalStep.SignUserOp:
1991
863
  return (
1992
- <SignUserOpStep
1993
- nextStep={
1994
- // skip withdrawal if use existing balance
1995
- selectedToken.holding
1996
- ? WithdrawalStep.InitiateWithdrawal
1997
- : WithdrawalStep.TrackUserOp
1998
- }
864
+ <SharedSignUserOpStep
865
+ nextStep={WithdrawalStep.InitiateWithdrawal}
1999
866
  setStep={setCurrentStep}
2000
867
  setUserOp={setUserOp}
2001
868
  />
@@ -2011,9 +878,10 @@ const ExchangeFlow = ({
2011
878
  case WithdrawalStep.TrackUserOp:
2012
879
  return (
2013
880
  <TrackUserOpStep
2014
- selectedToken={selectedToken}
2015
881
  userOp={userOp}
2016
- setStep={setCurrentStep}
882
+ onReset={() =>
883
+ setCurrentStep(WithdrawalStep.CheckSessionKey)
884
+ }
2017
885
  />
2018
886
  );
2019
887
  default:
@@ -2027,23 +895,17 @@ const ExchangeFlow = ({
2027
895
  <HeaderWrapper>
2028
896
  {!(
2029
897
  enforceFlow &&
2030
- (currentStep === WithdrawalStep.ChooseExchange ||
2031
- currentStep === WithdrawalStep.ChooseBalanceAsset)
898
+ currentStep === WithdrawalStep.ChooseExchange
2032
899
  ) && (
2033
900
  <IconButton
2034
901
  minWidth={"16px"}
2035
902
  minHeight={"16px"}
2036
903
  maxWidth={"16px"}
2037
904
  onClick={() => {
2038
- const index =
2039
- (selectedIntegration?.type === "delayed"
2040
- ? balanceSteps
2041
- : withdrawalSteps
2042
- ).findIndex(
2043
- (step) => step === currentStep,
2044
- ) - 1;
2045
- if (index >= 0) {
2046
- setCurrentStep(withdrawalSteps[index]);
905
+ const previousStep =
906
+ withdrawalPreviousStep[currentStep];
907
+ if (previousStep !== undefined) {
908
+ setCurrentStep(previousStep);
2047
909
  } else {
2048
910
  setFlow("");
2049
911
  }
@@ -2051,7 +913,7 @@ const ExchangeFlow = ({
2051
913
  >
2052
914
  <Icon
2053
915
  as={ChevronLeft}
2054
- color="gray"
916
+ color="fg.muted"
2055
917
  width={"16px"}
2056
918
  height={"16px"}
2057
919
  />
@@ -2080,7 +942,7 @@ const ExchangeFlow = ({
2080
942
  >
2081
943
  <Icon
2082
944
  as={X}
2083
- color="gray"
945
+ color="fg.muted"
2084
946
  width={"16px"}
2085
947
  height={"16px"}
2086
948
  />
@@ -2089,33 +951,9 @@ const ExchangeFlow = ({
2089
951
  </HeaderWrapper>
2090
952
  </Modal.Header>
2091
953
  <Modal.Body>
2092
- {/*{wrongChain ? (*/}
2093
- {/* <BodyWrapper>*/}
2094
- {/* <Box*/}
2095
- {/* display="flex"*/}
2096
- {/* flexDirection="column"*/}
2097
- {/* alignItems="center"*/}
2098
- {/* gap="16px"*/}
2099
- {/* textAlign="center"*/}
2100
- {/* >*/}
2101
- {/* <Text fontSize="16px" fontWeight="600">*/}
2102
- {/* Wrong Network*/}
2103
- {/* </Text>*/}
2104
- {/* <Text fontSize="14px" color="fg.muted">*/}
2105
- {/* Please switch to {getChainName(chainIdOut)} to*/}
2106
- {/* continue with your Binance withdrawal.*/}
2107
- {/* </Text>*/}
2108
- {/* <Button*/}
2109
- {/* onClick={() => {*/}
2110
- {/* switchChain({ chainId: chainIdOut });*/}
2111
- {/* }}*/}
2112
- {/* >*/}
2113
- {/* Switch to {getChainName(chainIdOut)}*/}
2114
- {/* </Button>*/}
2115
- {/* </Box>*/}
2116
- {/* </BodyWrapper>*/}
2117
- {/*) : */}
2118
- {currentStepComponent}
954
+ <AnimatedStep key={currentStep}>
955
+ {currentStepComponent}
956
+ </AnimatedStep>
2119
957
  </Modal.Body>
2120
958
  </>
2121
959
  );