@ensofinance/checkout-widget 0.1.8 → 0.1.9

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 (61) hide show
  1. package/dist/checkout-widget.es.js +24133 -25422
  2. package/dist/checkout-widget.umd.js +59 -65
  3. package/dist/index.d.ts +1 -3
  4. package/package.json +2 -2
  5. package/src/assets/usdc.webp +0 -0
  6. package/src/assets/usdt.webp +0 -0
  7. package/src/components/AmountInput.tsx +25 -41
  8. package/src/components/ChakraProvider.tsx +13 -36
  9. package/src/components/Checkout.tsx +0 -3
  10. package/src/components/CurrencySwapDisplay.tsx +22 -59
  11. package/src/components/DepositProcessing.tsx +1 -1
  12. package/src/components/ExchangeConfirmSecurity.tsx +1 -1
  13. package/src/components/QuoteParameters.tsx +1 -1
  14. package/src/components/TransactionDetailRow.tsx +2 -2
  15. package/src/components/cards/ExchangeCard.tsx +1 -1
  16. package/src/components/cards/OptionCard.tsx +1 -2
  17. package/src/components/cards/WalletCard.tsx +1 -1
  18. package/src/components/modal.tsx +3 -3
  19. package/src/components/steps/ExchangeFlow.tsx +1404 -231
  20. package/src/components/steps/FlowSelector.tsx +60 -117
  21. package/src/components/steps/WalletFlow/WalletAmountStep.tsx +2 -2
  22. package/src/components/steps/WalletFlow/WalletConfirmStep.tsx +51 -92
  23. package/src/components/steps/WalletFlow/WalletFlow.tsx +16 -17
  24. package/src/components/steps/WalletFlow/WalletQuoteStep.tsx +2 -2
  25. package/src/components/steps/WalletFlow/WalletTokenStep.tsx +4 -6
  26. package/src/components/ui/index.tsx +6 -23
  27. package/src/components/ui/toaster.tsx +1 -2
  28. package/src/types/index.ts +0 -97
  29. package/src/util/constants.tsx +0 -27
  30. package/src/util/enso-hooks.tsx +61 -75
  31. package/dist/checkout-widget.es.js.map +0 -1
  32. package/dist/checkout-widget.umd.js.map +0 -1
  33. package/src/assets/providers/alchemypay.svg +0 -21
  34. package/src/assets/providers/banxa.svg +0 -21
  35. package/src/assets/providers/binanceconnect.svg +0 -14
  36. package/src/assets/providers/kryptonim.svg +0 -6
  37. package/src/assets/providers/mercuryo.svg +0 -21
  38. package/src/assets/providers/moonpay.svg +0 -14
  39. package/src/assets/providers/stripe.svg +0 -16
  40. package/src/assets/providers/swapped.svg +0 -1
  41. package/src/assets/providers/topper.svg +0 -14
  42. package/src/assets/providers/transak.svg +0 -21
  43. package/src/assets/providers/unlimit.svg +0 -21
  44. package/src/components/steps/CardBuyFlow/CardBuyFlow.tsx +0 -412
  45. package/src/components/steps/CardBuyFlow/ChooseAmountStep.tsx +0 -352
  46. package/src/components/steps/CardBuyFlow/OpenWidgetStep.tsx +0 -193
  47. package/src/components/steps/SmartAccountFlow.tsx +0 -372
  48. package/src/components/steps/shared/ChooseAmountStep.tsx +0 -325
  49. package/src/components/steps/shared/SignUserOpStep.tsx +0 -117
  50. package/src/components/steps/shared/TrackUserOpStep.tsx +0 -625
  51. package/src/components/steps/shared/exchangeIntegration.ts +0 -19
  52. package/src/components/steps/shared/types.ts +0 -22
  53. package/src/components/ui/transitions.tsx +0 -16
  54. package/src/enso-api/model/bridgeTransactionResponse.ts +0 -37
  55. package/src/enso-api/model/bridgeTransactionResponseStatus.ts +0 -25
  56. package/src/enso-api/model/ensoEvent.ts +0 -30
  57. package/src/enso-api/model/ensoMetadata.ts +0 -23
  58. package/src/enso-api/model/layerZeroControllerCheckBridgeTransactionParams.ts +0 -21
  59. package/src/enso-api/model/layerZeroMessageStatus.ts +0 -39
  60. package/src/enso-api/model/refundDetails.ts +0 -21
  61. package/src/util/meld-hooks.tsx +0 -533
@@ -5,17 +5,14 @@ import {
5
5
  Icon,
6
6
  Text,
7
7
  Flex,
8
+ Skeleton,
9
+ Image,
10
+ Table,
8
11
  } from "@chakra-ui/react";
9
12
  import { ChevronLeft, X, TriangleAlert } from "lucide-react";
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";
13
+ import { useContext, useEffect, useMemo, useState, useCallback } from "react";
14
+ import { useAccount, useSignMessage } from "wagmi";
15
+ import { getUserOperationHash } from "viem/account-abstraction";
19
16
  import {
20
17
  BodyWrapper,
21
18
  HeaderDescription,
@@ -23,13 +20,10 @@ import {
23
20
  HeaderWrapper,
24
21
  ListWrapper,
25
22
  } from "../ui/styled";
26
- import { IconButton, Button } from "../ui";
23
+ import { IconButton, Button, Input } from "../ui";
24
+ import { AmountInput, AmountInputValue } from "../AmountInput";
27
25
  import { CheckoutContext } from "../Checkout";
28
26
  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";
33
27
  import {
34
28
  AccessTokenPayload,
35
29
  createLink,
@@ -38,26 +32,69 @@ import {
38
32
  import { useAppStore } from "@/store";
39
33
  import { AssetCard } from "../cards";
40
34
  import {
35
+ denormalizeValue,
41
36
  formatNumber,
42
37
  formatUSD,
43
38
  normalizeValue,
44
39
  } from "@/util";
45
- import { useTokenFromListBySymbols } from "@/util/common";
46
40
  import {
41
+ useTokenFromListBySymbols,
42
+ precisionizeNumber,
43
+ getPositiveDecimalValue,
44
+ } from "@/util/common";
45
+ import {
46
+ EXCHANGE_MAX_LIMIT_GAP_USD,
47
+ EXCHANGE_MIN_LIMIT,
47
48
  getCexIntermediateChain,
48
49
  DEFAULT_CEX_BRIDGE_CHAIN_MAPPING,
49
- CHECKOUT_BFF_URL,
50
50
  } from "@/util/constants";
51
- import { useAppDetails } from "@/util/enso-hooks";
52
- import { ConfirmExchangeStep } from "../ExchangeConfirmSecurity";
53
51
  import {
54
- ExchangeToIntegrationType,
55
- EXCHANGE_ICON_BY_TYPE,
56
- } from "./shared/exchangeIntegration";
57
- import type { MatchedToken, CryptocurrencyPosition, SupportedToken } from "./shared/types";
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";
59
+ import { ConfirmExchangeStep } from "../ExchangeConfirmSecurity";
60
+
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
+ };
58
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
+ };
59
88
 
60
89
  // 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
+
61
98
  interface HoldingsContent {
62
99
  equityPositions: any[];
63
100
  cryptocurrencyPositions: CryptocurrencyPosition[];
@@ -82,30 +119,46 @@ interface HoldingsResponse {
82
119
  errorType: string;
83
120
  }
84
121
 
85
- type MeshRequestError = Error & {
86
- code?: string;
87
- };
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";
88
141
 
89
142
  /*
90
143
  Withdrawal steps:
91
- 1. Check if session key is available
92
- 2. Perform auth if not available (optional)
144
+ 1. Check if session key is available
145
+ 2. Perform auth if not availble (optional)
93
146
  3. Get holdings and show token selector
94
147
  4. Select amount
95
- 5. Get userOp signature
96
- 6. Open transfer modal with amount and token
148
+ 6. Get userOp signature
149
+ 7. Open transfer modal with amount and token
97
150
  */
98
151
 
99
152
  export enum WithdrawalStep {
100
- ChooseExchange,
101
153
  CheckSessionKey,
154
+ ChooseExchange,
102
155
  ChooseExchangeAsset,
156
+ ChooseBalanceAsset,
103
157
  ChooseAmount,
104
158
  SignUserOp,
105
159
  InitiateWithdrawal,
106
160
  TrackUserOp,
107
161
  }
108
-
109
162
  const withdrawalPreviousStep: Partial<Record<WithdrawalStep, WithdrawalStep>> =
110
163
  {
111
164
  [WithdrawalStep.CheckSessionKey]: WithdrawalStep.ChooseExchange,
@@ -114,6 +167,11 @@ const withdrawalPreviousStep: Partial<Record<WithdrawalStep, WithdrawalStep>> =
114
167
  [WithdrawalStep.SignUserOp]: WithdrawalStep.ChooseAmount,
115
168
  [WithdrawalStep.InitiateWithdrawal]: WithdrawalStep.SignUserOp,
116
169
  };
170
+ const balancePreviousStep: Partial<Record<WithdrawalStep, WithdrawalStep>> = {
171
+ [WithdrawalStep.ChooseAmount]: WithdrawalStep.ChooseBalanceAsset,
172
+ [WithdrawalStep.SignUserOp]: WithdrawalStep.ChooseAmount,
173
+ };
174
+ // Integration details are fetched dynamically from Mesh API.
117
175
 
118
176
  // Mesh network IDs for EVM chains (from Mesh networks API)
119
177
  const MESH_NETWORK_IDS: { [chainId: number]: string } = {
@@ -125,6 +183,8 @@ const MESH_NETWORK_IDS: { [chainId: number]: string } = {
125
183
  43114: "bad16371-c22a-4bf4-a311-274d046cd760", // Avalanche C-Chain
126
184
  56: "ed0ebeec-b166-4c8b-8574-cb078f7af8cf", // BSC
127
185
  146: "385f0b3a-8471-4b8f-884f-c4f4496f1603", // Sonic
186
+ // 81457: "0c17e03f-77fa-4644-b84c-eb247af8c4c1", // Blast
187
+ // 11155111: "03b2d786-7092-4a6a-9737-d6013e21819b", // Sepolia (testnet)
128
188
  };
129
189
 
130
190
  const MESH_NETWORKS = Object.keys(MESH_NETWORK_IDS).map(Number);
@@ -133,20 +193,6 @@ const getNetworkId = (chainId: number): string => {
133
193
  return MESH_NETWORK_IDS[chainId] || MESH_NETWORK_IDS[8453]; // Default to Base
134
194
  };
135
195
 
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
-
150
196
  const useHandleMeshAccessPayload = () => {
151
197
  const { setMeshAccessToken } = useAppStore();
152
198
  const deviceKey = useDeviceId();
@@ -154,12 +200,12 @@ const useHandleMeshAccessPayload = () => {
154
200
 
155
201
  return useCallback(
156
202
  (accessTokenPayload: AccessTokenPayload, sessionId: string) => {
157
- setMeshAccessToken(accessTokenPayload);
203
+ setMeshAccessToken(accessTokenPayload); // Persist access token and session id for future reloads
158
204
 
159
205
  sessionStorage.setItem(
160
206
  `${deviceKey}:${selectedIntegration?.type}`,
161
207
  JSON.stringify({
162
- accessTokenPayload,
208
+ accessTokenPayload, // Store full object for proper restoration
163
209
  sessionId,
164
210
  timestamp: Date.now(),
165
211
  }),
@@ -171,7 +217,7 @@ const useHandleMeshAccessPayload = () => {
171
217
 
172
218
  type MeshIntegration = {
173
219
  id: string;
174
- type: string;
220
+ type: string; // brokerType
175
221
  name: string;
176
222
  networks?: {
177
223
  id: string;
@@ -187,6 +233,9 @@ const ChooseExchangeStep = ({
187
233
  setStep: (step: WithdrawalStep) => void;
188
234
  }) => {
189
235
  const { chainIdOut, setChainIdIn } = useAppStore();
236
+ const [integrations, setIntegrations] = useState<MeshIntegration[]>([]);
237
+ const [loading, setLoading] = useState(true);
238
+ const [error, setError] = useState<string | null>(null);
190
239
  const setSelectedIntegration = useAppStore(
191
240
  (state) => state.setSelectedIntegration,
192
241
  );
@@ -195,50 +244,42 @@ const ChooseExchangeStep = ({
195
244
  const cexMapping =
196
245
  cexBridgeChainMapping ?? DEFAULT_CEX_BRIDGE_CHAIN_MAPPING;
197
246
 
247
+ // Use intermediate chain for filtering if target chain needs bridging
198
248
  const effectiveChainId =
199
249
  getCexIntermediateChain(chainIdOut, cexMapping) ?? chainIdOut;
200
250
 
201
- const availableExchanges = useMemo(
202
- () =>
203
- (enableExchange ?? [])
204
- .map((exchange) => ExchangeToIntegrationType[exchange])
205
- .filter(Boolean),
206
- [enableExchange],
207
- );
208
-
251
+ // Set chainIdIn to effective chain for cross-chain tracking
209
252
  useEffect(() => {
210
253
  effectiveChainId ? setChainIdIn(effectiveChainId) : chainIdOut;
211
254
  }, [effectiveChainId, setChainIdIn]);
212
255
 
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");
229
- }
230
-
231
- const data = (await res.json()) as MeshIntegration[];
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
+ );
232
272
 
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
- });
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);
279
+ }
280
+ };
281
+ fetchIntegrations();
282
+ }, [effectiveChainId]);
242
283
 
243
284
  if (loading)
244
285
  return (
@@ -250,18 +291,23 @@ const ChooseExchangeStep = ({
250
291
  return (
251
292
  <BodyWrapper>
252
293
  <Box p={5} color="red.500">
253
- Failed to load exchanges
294
+ {error}
254
295
  </Box>
255
296
  </BodyWrapper>
256
297
  );
257
298
 
258
299
  return (
259
300
  <BodyWrapper>
301
+ {/*<Box mb={4} width="100%" textAlign="left">*/}
302
+ {/* <HeaderTitle>Choose Exchange</HeaderTitle>*/}
303
+ {/*</Box>*/}
304
+
260
305
  {integrations?.length > 0 ? (
261
306
  <ListWrapper>
262
307
  {integrations.map((integration) => (
263
308
  <AssetCard
264
309
  key={integration.id}
310
+ // chainId={chainIdOut || 1}
265
311
  icon={EXCHANGE_ICON_BY_TYPE[integration.type]}
266
312
  title={integration.name}
267
313
  balance={""}
@@ -306,20 +352,26 @@ const CheckSessionKeyStep = ({
306
352
  const [showConfirmation, setShowConfirmation] = useState(false);
307
353
  const selectedIntegration = useAppStore((s) => s.selectedIntegration);
308
354
 
355
+ // Bridging is required if chainIdIn differs from chainIdOut
309
356
  const needsBridging = chainIdIn !== chainIdOut;
357
+
358
+ // Invalid only if chain is not in MESH_NETWORKS AND cannot be bridged
310
359
  const invalidChainId =
311
360
  chainIdOut && !MESH_NETWORKS.includes(chainIdOut) && !needsBridging;
312
361
  const handleMeshAccessPayload = useHandleMeshAccessPayload();
313
362
 
314
363
  useEffect(() => {
315
364
  if (!selectedIntegration) {
365
+ // ensure an exchange is selected
316
366
  setStep(WithdrawalStep.ChooseExchange);
317
367
  return;
318
368
  }
319
369
  if (invalidChainId) return;
370
+ // If connection is persisted, skip fetching a new link token
320
371
  const saved = sessionStorage.getItem(
321
372
  `${deviceKey}:${selectedIntegration.type}`,
322
373
  );
374
+ // On load: check for persisted connection and hydrate state
323
375
  if (saved) {
324
376
  try {
325
377
  const parsed = JSON.parse(saved);
@@ -330,17 +382,19 @@ const CheckSessionKeyStep = ({
330
382
  return;
331
383
  }
332
384
  } catch (e) {
385
+ // ignore malformed storage
333
386
  console.error("Failed to parse saved Mesh connection", e);
334
387
  }
335
388
  }
336
389
 
390
+ // Show confirmation instead of auto-connecting
337
391
  setShowConfirmation(true);
338
392
  }, [deviceKey, invalidChainId, selectedIntegration]);
339
393
 
340
394
  const handleConfirmAuth = () => {
341
395
  const brokerType = selectedIntegration?.type;
342
396
  fetch(
343
- `${CHECKOUT_BFF_URL}/linktoken?brokerType=${encodeURIComponent(brokerType)}`,
397
+ `${MESH_API_URL}/linktoken?brokerType=${encodeURIComponent(brokerType)}`,
344
398
  {
345
399
  method: "POST",
346
400
  headers: {
@@ -366,7 +420,7 @@ const CheckSessionKeyStep = ({
366
420
  handleMeshAccessPayload(
367
421
  payload.accessToken,
368
422
  response.content.sessionId,
369
- );
423
+ ); // Persist access token and session id for future reloads
370
424
  },
371
425
  onExit: (error) => {
372
426
  console.log("onExit", error);
@@ -423,6 +477,21 @@ const CheckSessionKeyStep = ({
423
477
  return <Spinner m={5} />;
424
478
  };
425
479
 
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
+
426
495
  const ChooseAssetStep = ({
427
496
  setStep,
428
497
  onTokenSelect,
@@ -430,9 +499,17 @@ const ChooseAssetStep = ({
430
499
  setStep: (step: WithdrawalStep) => void;
431
500
  onTokenSelect: (token: MatchedToken) => void;
432
501
  }) => {
502
+ // const [holdings, setHoldings] = useState<CryptocurrencyPosition[]>([]);
503
+ // const [supportedTokens, setSupportedTokens] = useState<SupportedToken[]>(
504
+ // [],
505
+ // );
506
+ const [matchedTokens, setMatchedTokens] = useState<MatchedToken[]>([]);
433
507
  const [selectedTokenSymbol, setSelectedTokenSymbol] = useState<
434
508
  string | null
435
509
  >(null);
510
+ const [loading, setLoading] = useState(true);
511
+ const [error, setError] = useState<string | null>(null);
512
+ const { address } = useAccount();
436
513
  const {
437
514
  meshAccessToken,
438
515
  sessionId,
@@ -445,115 +522,108 @@ const ChooseAssetStep = ({
445
522
 
446
523
  const selectedIntegration = useAppStore((s) => s.selectedIntegration);
447
524
 
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 || "",
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
+ }),
468
543
  },
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";
489
- }
490
- throw meshError;
491
- }
492
-
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",
504
544
  );
505
- }
506
545
 
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,
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}`,
513
561
  );
514
- if (holding) {
515
- return {
516
- ...token,
517
- balance: holding.amount,
518
- marketValue: holding.marketValue,
519
- holding: holding,
520
- } as MatchedToken;
521
562
  }
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
- });
563
+ throw new Error(
564
+ holdingsData.message || "Failed to fetch holdings",
565
+ );
566
+ }
538
567
 
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
- ]);
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",
617
+ );
618
+ } finally {
619
+ setLoading(false);
620
+ }
621
+ };
622
+
623
+ if (meshAccessToken && sessionId && chainIdIn && selectedIntegration) {
624
+ fetchData();
625
+ }
626
+ }, [address, chainIdIn, meshAccessToken, sessionId, selectedIntegration]);
557
627
 
558
628
  const geckoTokens = useTokenFromListBySymbols(
559
629
  matchedTokens.map((token) => token.symbol),
@@ -569,10 +639,7 @@ const ChooseAssetStep = ({
569
639
  if (error)
570
640
  return (
571
641
  <Box p={5} color="red.500">
572
- Error:{" "}
573
- {error instanceof Error
574
- ? error.message
575
- : "Failed to fetch data"}
642
+ Error: {error}
576
643
  </Box>
577
644
  );
578
645
 
@@ -601,24 +668,19 @@ const ChooseAssetStep = ({
601
668
  loading={false}
602
669
  selected={selectedTokenSymbol === token.symbol}
603
670
  onClick={() => {
604
- const tokenAddress =
605
- geckoTokens?.[index]?.address;
606
671
  setSelectedTokenSymbol(token.symbol);
607
- onTokenSelect({
608
- ...token,
609
- tokenAddress,
610
- });
611
- setTokenIn(tokenAddress);
672
+ onTokenSelect(token);
673
+ setTokenIn(geckoTokens?.[index]?.address);
612
674
  }}
613
675
  />
614
676
  ))}
615
677
  </ListWrapper>
616
678
  </Box>
617
- {matchedTokens.length === 0 ? (
679
+ {matchedTokens.length === 0 && (
618
680
  <Box textAlign="center" color="fg.subtle" py={8}>
619
681
  No tokens with balances found for this chain
620
682
  </Box>
621
- ) : null}
683
+ )}
622
684
  {
623
685
  <Button
624
686
  disabled={!selectedTokenSymbol}
@@ -633,6 +695,441 @@ const ChooseAssetStep = ({
633
695
  );
634
696
  };
635
697
 
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(
905
+ denormalizeValue(normalizedAmount, tokenInData.decimals),
906
+ );
907
+ } catch (error) {
908
+ setAmountIn("0");
909
+ }
910
+ }, [amount, tokenInData?.decimals, setAmountIn]);
911
+
912
+ const numericAmount = getPositiveDecimalValue(amount);
913
+ const hasPositiveAmount = numericAmount !== null;
914
+ const hasUsdValue = !!usdValue && usdValue !== ".";
915
+ const notEnoughBalance = selectedToken
916
+ ? hasPositiveAmount &&
917
+ numericAmount !== null &&
918
+ numericAmount > selectedToken.balance
919
+ : true;
920
+
921
+ // Limits validation logic - only for CEX withdrawals
922
+ const currentUsdValue = hasUsdValue
923
+ ? (getPositiveDecimalValue(usdValue) ?? 0)
924
+ : 0;
925
+ const minValueForToken =
926
+ isWithdrawal && selectedToken
927
+ ? EXCHANGE_MIN_LIMIT[
928
+ selectedToken.symbol as keyof typeof EXCHANGE_MIN_LIMIT
929
+ ]
930
+ : 0;
931
+
932
+ const isBelowMinAmount =
933
+ isWithdrawal &&
934
+ selectedToken &&
935
+ hasPositiveAmount &&
936
+ currentUsdValue > 0 &&
937
+ minValueForToken &&
938
+ numericAmount !== null &&
939
+ numericAmount < minValueForToken;
940
+ const isAboveMaxAmount =
941
+ isWithdrawal &&
942
+ selectedToken &&
943
+ hasPositiveAmount &&
944
+ currentUsdValue > 0 &&
945
+ currentUsdValue > +maxUsdAmount;
946
+
947
+ const isAmountInvalid =
948
+ !hasPositiveAmount ||
949
+ isBelowMinAmount ||
950
+ isAboveMaxAmount ||
951
+ notEnoughBalance;
952
+
953
+ if (!selectedToken) {
954
+ return (
955
+ <BodyWrapper>
956
+ <Box textAlign="center" color="fg.subtle" py={8}>
957
+ No token selected
958
+ </Box>
959
+ </BodyWrapper>
960
+ );
961
+ }
962
+
963
+ return (
964
+ <BodyWrapper>
965
+ <Box mb={4} width="100%" textAlign="left">
966
+ <HeaderTitle>Enter Amount</HeaderTitle>
967
+ <HeaderDescription>
968
+ Available: {formatNumber(selectedToken.balance)}{" "}
969
+ {selectedToken.symbol} (
970
+ {formatUSD(selectedToken.marketValue)})
971
+ </HeaderDescription>
972
+ </Box>
973
+
974
+ <Box
975
+ display={"flex"}
976
+ flexDirection={"column"}
977
+ gap={"8px"}
978
+ width="100%"
979
+ >
980
+ <AmountInput
981
+ value={amountInput}
982
+ onChange={setAmountInput}
983
+ tokenSymbol={selectedToken.symbol}
984
+ tokenPriceUsd={tokenPriceUsd}
985
+ roundingPrecision={roundingPrecision}
986
+ onPercentSelect={getPercentAmounts}
987
+ />
988
+ </Box>
989
+
990
+ {
991
+ <Box
992
+ textAlign="center"
993
+ color="fg.subtle"
994
+ fontSize="xs"
995
+ h={3}
996
+ m={-1}
997
+ visibility={isAmountInvalid ? "visible" : "hidden"}
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
+
636
1133
  const InitiateWithdrawalStep = ({
637
1134
  selectedToken,
638
1135
  userOp,
@@ -659,6 +1156,7 @@ const InitiateWithdrawalStep = ({
659
1156
  return;
660
1157
  }
661
1158
 
1159
+ // Convert amountIn from wei to token amount
662
1160
  const transferAmount = tokenInData?.decimals
663
1161
  ? normalizeValue(amountIn, tokenInData.decimals)
664
1162
  : 0;
@@ -675,16 +1173,19 @@ const InitiateWithdrawalStep = ({
675
1173
  amount: transferAmount,
676
1174
  },
677
1175
  ];
678
-
679
- console.log("link request body", {
1176
+ const meshData = {
680
1177
  restrictMultipleAccounts: true,
681
1178
  userId: deviceKey,
682
1179
  integrationId: selectedIntegration?.id,
683
- transferOptions: { toAddresses },
684
- });
1180
+ transferOptions: {
1181
+ toAddresses,
1182
+ },
1183
+ };
1184
+
1185
+ console.log("link request body", meshData);
685
1186
 
686
1187
  const response = await fetch(
687
- `${CHECKOUT_BFF_URL}/linktoken?brokerType=${encodeURIComponent(selectedIntegration?.type || "")}`,
1188
+ `${MESH_API_URL}/linktoken?brokerType=${encodeURIComponent(selectedIntegration?.type || "")}`,
688
1189
  {
689
1190
  method: "POST",
690
1191
  headers: {
@@ -721,7 +1222,7 @@ const InitiateWithdrawalStep = ({
721
1222
  clientId: address,
722
1223
  accessTokens,
723
1224
  onIntegrationConnected: (payload) => {
724
- handleMeshAccessPayload(payload.accessToken, sessionId);
1225
+ handleMeshAccessPayload(payload.accessToken, sessionId); // Persist access token and session id for future reloads
725
1226
  console.log("Integration connected", payload);
726
1227
  },
727
1228
  onTransferFinished: (transferData) => {
@@ -817,24 +1318,657 @@ const InitiateWithdrawalStep = ({
817
1318
  );
818
1319
  };
819
1320
 
1321
+ // Phase indicator component for cross-chain tracking
1322
+ const PhaseIndicator = ({
1323
+ currentPhase,
1324
+ phases,
1325
+ }: {
1326
+ currentPhase: number;
1327
+ phases: string[];
1328
+ }) => {
1329
+ return (
1330
+ <Box display="flex" gap={4} justifyContent="center" mb={4}>
1331
+ {phases.map((phase, index) => (
1332
+ <Box key={phase} display="flex" alignItems="center" gap={2}>
1333
+ <Box
1334
+ w={6}
1335
+ h={6}
1336
+ borderRadius="full"
1337
+ bg={
1338
+ index < currentPhase
1339
+ ? "green.500"
1340
+ : index === currentPhase
1341
+ ? "blue.500"
1342
+ : "gray.300"
1343
+ }
1344
+ display="flex"
1345
+ alignItems="center"
1346
+ justifyContent="center"
1347
+ color="white"
1348
+ fontSize="xs"
1349
+ fontWeight="bold"
1350
+ >
1351
+ {index < currentPhase ? "✓" : index + 1}
1352
+ </Box>
1353
+ <Text
1354
+ fontSize="sm"
1355
+ color={index === currentPhase ? "fg" : "fg.muted"}
1356
+ fontWeight={
1357
+ index === currentPhase ? "semibold" : "normal"
1358
+ }
1359
+ >
1360
+ {phase}
1361
+ </Text>
1362
+ </Box>
1363
+ ))}
1364
+ </Box>
1365
+ );
1366
+ };
1367
+
1368
+ const TrackUserOpStep = ({
1369
+ selectedToken,
1370
+ userOp,
1371
+ setStep,
1372
+ }: {
1373
+ selectedToken: MatchedToken | null;
1374
+ userOp: any;
1375
+ setStep: (step: WithdrawalStep) => void;
1376
+ }) => {
1377
+ const { chainIdIn, chainIdOut, tokenInData } = useAppDetails();
1378
+ const { amountIn } = useAppStore();
1379
+
1380
+ // Determine if this is a cross-chain (bridge) flow
1381
+ const isCrosschain = chainIdIn !== chainIdOut;
1382
+
1383
+ // Phase management: for crosschain 'cex' -> 'bridge' -> 'completed', for single-chain just 'cex' -> 'completed'
1384
+ const [phase, setPhase] = useState<
1385
+ "cex" | "bridge" | "completed" | "failed"
1386
+ >("cex");
1387
+ const [operationId, setOperationId] = useState<string | null>(null);
1388
+ const [status, setStatus] = useState<
1389
+ "sending" | "tracking" | "completed" | "failed"
1390
+ >("sending");
1391
+ const [message, setMessage] = useState("Sending operation to tracker...");
1392
+ const [txHash, setTxHash] = useState<`0x${string}` | null>(null);
1393
+ const [isTimerFinished, setIsTimerFinished] = useState(false);
1394
+ const [trackingInterval, setTrackingInterval] =
1395
+ useState<NodeJS.Timeout | null>(null);
1396
+ const [destinationVerified, setDestinationVerified] = useState(false);
1397
+ const [destinationSuccess, setDestinationSuccess] = useState<
1398
+ boolean | null
1399
+ >(null);
1400
+ const [refundDetails, setRefundDetails] = useState<{
1401
+ token: string;
1402
+ amount: string;
1403
+ recipient: string;
1404
+ isNative: boolean;
1405
+ } | null>(null);
1406
+ const [destinationTxHash, setDestinationTxHash] = useState<string | null>(
1407
+ null,
1408
+ );
1409
+
1410
+ // LayerZero tracking for bridge progress (real-time updates)
1411
+ const lzStatus = useLayerZeroStatus(
1412
+ txHash ?? undefined,
1413
+ isCrosschain && phase === "bridge",
1414
+ );
1415
+
1416
+ // Send UserOp to tracker
1417
+ useEffect(() => {
1418
+ const sendUserOpToTracker = async () => {
1419
+ if (!selectedToken || !userOp || !tokenInData) {
1420
+ console.error("Missing required data for tracking");
1421
+ setStatus("failed");
1422
+ setMessage("Missing required data");
1423
+ setPhase("failed");
1424
+ return;
1425
+ }
1426
+
1427
+ try {
1428
+ const response = await fetch(
1429
+ "https://alpha-scanners-dev-054573dc8549.herokuapp.com/operations",
1430
+ {
1431
+ method: "POST",
1432
+ headers: {
1433
+ "Content-Type": "application/json",
1434
+ },
1435
+ body: JSON.stringify({
1436
+ userOperationData: {
1437
+ sender: userOp.sender,
1438
+ nonce: userOp.nonce,
1439
+ factory: userOp.factory,
1440
+ factoryData: userOp.factoryData,
1441
+ callData: userOp.callData,
1442
+ callGasLimit: userOp.callGasLimit,
1443
+ verificationGasLimit:
1444
+ userOp.verificationGasLimit,
1445
+ preVerificationGas: userOp.preVerificationGas,
1446
+ maxFeePerGas: userOp.maxFeePerGas,
1447
+ maxPriorityFeePerGas:
1448
+ userOp.maxPriorityFeePerGas,
1449
+ paymaster: userOp.paymaster,
1450
+ paymasterData: userOp.paymasterData,
1451
+ paymasterVerificationGasLimit:
1452
+ userOp.paymasterVerificationGasLimit,
1453
+ paymasterPostOpGasLimit:
1454
+ userOp.paymasterPostOpGasLimit,
1455
+ signature: userOp.signature,
1456
+ },
1457
+ chainId: chainIdIn,
1458
+ expectedBalance: amountIn,
1459
+ tokenAddress: tokenInData.address,
1460
+ }),
1461
+ },
1462
+ );
1463
+
1464
+ const data = await response.json();
1465
+ console.log("Operation tracking response:", data);
1466
+
1467
+ if (data.success && data.operationId) {
1468
+ setOperationId(data.operationId);
1469
+ setStatus("tracking");
1470
+ setMessage(
1471
+ isCrosschain
1472
+ ? "Funds forwarding in progress..."
1473
+ : "Tracking operation progress...",
1474
+ );
1475
+ } else {
1476
+ throw new Error(
1477
+ data.message || "Failed to send operation to tracker",
1478
+ );
1479
+ }
1480
+ } catch (error) {
1481
+ console.error("Failed to send operation to tracker:", error);
1482
+ setStatus("failed");
1483
+ setMessage("Failed to send operation to tracker");
1484
+ setPhase("failed");
1485
+ }
1486
+ };
1487
+
1488
+ sendUserOpToTracker();
1489
+ }, []);
1490
+
1491
+ // Track operation status
1492
+ useEffect(() => {
1493
+ if (!operationId || status !== "tracking") return;
1494
+
1495
+ const trackOperation = async () => {
1496
+ try {
1497
+ const response = await fetch(
1498
+ `https://alpha-scanners-dev-054573dc8549.herokuapp.com/operations/${operationId}/status`,
1499
+ );
1500
+ const data = await response.json();
1501
+ console.log("Operation status:", data);
1502
+
1503
+ if (data.operation?.status === "completed") {
1504
+ setStatus("completed");
1505
+
1506
+ if (isCrosschain) {
1507
+ // For crosschain: move to bridge phase
1508
+ setMessage("Funds forwarding completed!");
1509
+ if (data.operation?.bundleTxHash) {
1510
+ setTxHash(
1511
+ data.operation.bundleTxHash as `0x${string}`,
1512
+ );
1513
+ setPhase("bridge");
1514
+ } else {
1515
+ console.warn(
1516
+ "No bundleTxHash returned from indexer, cannot track bridge",
1517
+ );
1518
+ setPhase("completed");
1519
+ }
1520
+ } else {
1521
+ // For single-chain: complete
1522
+ setMessage("Operation completed successfully!");
1523
+ setPhase("completed");
1524
+ }
1525
+
1526
+ if (trackingInterval) {
1527
+ clearInterval(trackingInterval);
1528
+ setTrackingInterval(null);
1529
+ }
1530
+ } else if (
1531
+ ["failed", "failed_permanent"].includes(
1532
+ data.operation?.status,
1533
+ )
1534
+ ) {
1535
+ setStatus("failed");
1536
+ setMessage(
1537
+ isCrosschain
1538
+ ? "Bridging failed. Please select Smart-account balance as a source to use withdrawn funds"
1539
+ : "Operation failed",
1540
+ );
1541
+ setPhase("failed");
1542
+ if (trackingInterval) {
1543
+ clearInterval(trackingInterval);
1544
+ setTrackingInterval(null);
1545
+ }
1546
+ }
1547
+ } catch (error) {
1548
+ console.error("Failed to fetch operation status:", error);
1549
+ }
1550
+ };
1551
+
1552
+ const interval = setInterval(trackOperation, 3000);
1553
+ setTrackingInterval(interval);
1554
+
1555
+ return () => {
1556
+ if (interval) clearInterval(interval);
1557
+ };
1558
+ }, [operationId, status, isCrosschain]);
1559
+
1560
+ // Handle bridge completion - verify destination execution once LayerZero shows DELIVERED
1561
+ useEffect(() => {
1562
+ if (!isCrosschain || phase !== "bridge") return;
1563
+
1564
+ // If LayerZero failed, mark as failed immediately
1565
+ if (lzStatus.isFailed) {
1566
+ setPhase("failed");
1567
+ return;
1568
+ }
1569
+
1570
+ // When LayerZero shows DELIVERED, verify destination execution with Enso API
1571
+ if (
1572
+ lzStatus.isComplete &&
1573
+ !destinationVerified &&
1574
+ txHash &&
1575
+ chainIdIn
1576
+ ) {
1577
+ // TODO: use hook
1578
+ const verifyDestination = async () => {
1579
+ try {
1580
+ const res = await fetch(
1581
+ `https://api.enso.build/api/v1/layerzero/bridge/check?chainId=${chainIdIn}&txHash=${txHash}`,
1582
+ );
1583
+ if (!res.ok) {
1584
+ // If API call fails, assume success (LayerZero delivered)
1585
+ setDestinationVerified(true);
1586
+ setDestinationSuccess(true);
1587
+ setPhase("completed");
1588
+ return;
1589
+ }
1590
+ const data = await res.json();
1591
+ setDestinationVerified(true);
1592
+ setDestinationTxHash(data.destinationTxHash || null);
1593
+
1594
+ if (data.status === "success") {
1595
+ setDestinationSuccess(true);
1596
+ setPhase("completed");
1597
+ } else if (data.status === "failed") {
1598
+ setDestinationSuccess(false);
1599
+ setRefundDetails(
1600
+ data.ensoDestinationEvent?.refundDetails || null,
1601
+ );
1602
+ setPhase("failed");
1603
+ } else {
1604
+ // Still pending, assume success since LZ delivered
1605
+ setDestinationSuccess(true);
1606
+ setPhase("completed");
1607
+ }
1608
+ } catch (error) {
1609
+ console.error("Failed to verify destination:", error);
1610
+ // On error, assume success since LayerZero delivered
1611
+ setDestinationVerified(true);
1612
+ setDestinationSuccess(true);
1613
+ setPhase("completed");
1614
+ }
1615
+ };
1616
+ verifyDestination();
1617
+ }
1618
+ }, [
1619
+ phase,
1620
+ lzStatus.isComplete,
1621
+ lzStatus.isFailed,
1622
+ isCrosschain,
1623
+ destinationVerified,
1624
+ txHash,
1625
+ chainIdIn,
1626
+ ]);
1627
+
1628
+ const handleTimerFinish = () => {
1629
+ setIsTimerFinished(true);
1630
+ };
1631
+
1632
+ const getOverallStatus = () => {
1633
+ if (phase === "failed") return "failed";
1634
+ if (phase === "completed") return "completed";
1635
+ return "processing";
1636
+ };
1637
+
1638
+ const getStatusColor = () => {
1639
+ switch (getOverallStatus()) {
1640
+ case "completed":
1641
+ return "#14AE5C";
1642
+ case "failed":
1643
+ return "#E84142";
1644
+ default:
1645
+ return "#1E171F";
1646
+ }
1647
+ };
1648
+
1649
+ const getStatusText = () => {
1650
+ switch (getOverallStatus()) {
1651
+ case "completed":
1652
+ return "Success";
1653
+ case "failed":
1654
+ return "Failed";
1655
+ default:
1656
+ return "Processing";
1657
+ }
1658
+ };
1659
+
1660
+ const getCurrentMessage = () => {
1661
+ if (isCrosschain) {
1662
+ if (phase === "cex") {
1663
+ return `(1/2) ${message}`;
1664
+ } else if (phase === "bridge") {
1665
+ return `(2/2) ${lzStatus.message}`;
1666
+ } else if (phase === "completed") {
1667
+ return "Transfer completed successfully!";
1668
+ } else {
1669
+ return refundDetails
1670
+ ? "Destination execution failed. Funds refunded to smart account."
1671
+ : "Transfer failed";
1672
+ }
1673
+ } else {
1674
+ if (phase === "completed") {
1675
+ return "Operation completed successfully!";
1676
+ } else if (phase === "failed") {
1677
+ return "Operation failed";
1678
+ }
1679
+ return message;
1680
+ }
1681
+ };
1682
+
1683
+ const renderStatusIcon = () => {
1684
+ const isProcessing = phase === "cex" || phase === "bridge";
1685
+
1686
+ if (isProcessing) {
1687
+ return (
1688
+ <CircleTimer
1689
+ start={status === "tracking" || phase === "bridge"}
1690
+ onFinish={handleTimerFinish}
1691
+ duration={isCrosschain ? 180 : 120}
1692
+ />
1693
+ );
1694
+ }
1695
+
1696
+ if (phase === "completed") {
1697
+ return (
1698
+ <Box display="flex" flexDirection="column" alignItems="center">
1699
+ <Image
1700
+ src={SuccessIcon}
1701
+ boxShadow="0px 0px 20px #14AE5C"
1702
+ borderRadius="90%"
1703
+ width="58px"
1704
+ height="58px"
1705
+ />
1706
+ </Box>
1707
+ );
1708
+ }
1709
+
1710
+ return (
1711
+ <Box display="flex" flexDirection="column" alignItems="center">
1712
+ <Image
1713
+ src={FailIcon}
1714
+ boxShadow="0px 0px 20px #E84142"
1715
+ borderRadius="90%"
1716
+ width="58px"
1717
+ height="58px"
1718
+ />
1719
+ </Box>
1720
+ );
1721
+ };
1722
+
1723
+ const intermediateChainName = chainIdIn
1724
+ ? STARGATE_CHAIN_NAMES[chainIdIn as keyof typeof STARGATE_CHAIN_NAMES]
1725
+ : "Unknown";
1726
+ const targetChainName = chainIdOut
1727
+ ? STARGATE_CHAIN_NAMES[chainIdOut as keyof typeof STARGATE_CHAIN_NAMES]
1728
+ : "Unknown";
1729
+
1730
+ return (
1731
+ <BodyWrapper>
1732
+ {/* Phase Indicator (crosschain only) */}
1733
+ {isCrosschain && (
1734
+ <PhaseIndicator
1735
+ currentPhase={
1736
+ phase === "cex" ? 0 : phase === "bridge" ? 1 : 2
1737
+ }
1738
+ phases={["Forward funds", "Bridge"]}
1739
+ />
1740
+ )}
1741
+
1742
+ {/* Status Icon */}
1743
+ <Box
1744
+ display="flex"
1745
+ flexDirection="column"
1746
+ paddingBottom="16px"
1747
+ alignItems="center"
1748
+ width="100%"
1749
+ >
1750
+ {renderStatusIcon()}
1751
+ <Box
1752
+ display="flex"
1753
+ flexDirection="column"
1754
+ alignItems="center"
1755
+ marginTop="16px"
1756
+ textAlign="center"
1757
+ >
1758
+ <Text
1759
+ fontSize="lg"
1760
+ fontWeight="semibold"
1761
+ color="fg"
1762
+ marginBottom="8px"
1763
+ >
1764
+ {getCurrentMessage()}
1765
+ </Text>
1766
+ {(phase === "cex" || phase === "bridge") &&
1767
+ isTimerFinished && (
1768
+ <Text
1769
+ fontSize="sm"
1770
+ color="fg.muted"
1771
+ maxWidth="280px"
1772
+ >
1773
+ Your operation is being processed – no action is
1774
+ required from you.
1775
+ </Text>
1776
+ )}
1777
+ </Box>
1778
+ </Box>
1779
+
1780
+ {/* Status Table */}
1781
+ <Table.Root key="status" size="sm" variant="outline" width="100%">
1782
+ <Table.Body>
1783
+ <Table.Row>
1784
+ <Table.Cell>Status</Table.Cell>
1785
+ <Table.Cell
1786
+ display="flex"
1787
+ textAlign="end"
1788
+ justifyContent="end"
1789
+ >
1790
+ <Text color={getStatusColor()}>
1791
+ {getStatusText()}
1792
+ </Text>
1793
+ </Table.Cell>
1794
+ </Table.Row>
1795
+ {isCrosschain && (
1796
+ <>
1797
+ <Table.Row>
1798
+ <Table.Cell>Current Phase</Table.Cell>
1799
+ <Table.Cell
1800
+ display="flex"
1801
+ textAlign="end"
1802
+ justifyContent="end"
1803
+ >
1804
+ <Text>
1805
+ {phase === "cex"
1806
+ ? "Awaiting funds"
1807
+ : phase === "bridge"
1808
+ ? "Bridging"
1809
+ : phase === "completed"
1810
+ ? "Complete"
1811
+ : "Failed"}
1812
+ </Text>
1813
+ </Table.Cell>
1814
+ </Table.Row>
1815
+ <Table.Row>
1816
+ <Table.Cell>Intermediate Chain</Table.Cell>
1817
+ <Table.Cell
1818
+ display="flex"
1819
+ textAlign="end"
1820
+ justifyContent="end"
1821
+ >
1822
+ <Text textTransform="capitalize">
1823
+ {intermediateChainName}
1824
+ </Text>
1825
+ </Table.Cell>
1826
+ </Table.Row>
1827
+ <Table.Row>
1828
+ <Table.Cell>Final Destination</Table.Cell>
1829
+ <Table.Cell
1830
+ display="flex"
1831
+ textAlign="end"
1832
+ justifyContent="end"
1833
+ >
1834
+ <Text textTransform="capitalize">
1835
+ {targetChainName}
1836
+ </Text>
1837
+ </Table.Cell>
1838
+ </Table.Row>
1839
+ </>
1840
+ )}
1841
+ {operationId && (
1842
+ <Table.Row>
1843
+ <Table.Cell>Operation ID</Table.Cell>
1844
+ <Table.Cell
1845
+ display="flex"
1846
+ textAlign="end"
1847
+ justifyContent="end"
1848
+ >
1849
+ <Text fontSize="sm" color="fg.muted">
1850
+ {operationId}
1851
+ </Text>
1852
+ </Table.Cell>
1853
+ </Table.Row>
1854
+ )}
1855
+ {isCrosschain && txHash && (
1856
+ <Table.Row>
1857
+ <Table.Cell>Bridge TX</Table.Cell>
1858
+ <Table.Cell
1859
+ display="flex"
1860
+ textAlign="end"
1861
+ justifyContent="end"
1862
+ >
1863
+ <Text
1864
+ fontSize="sm"
1865
+ color="blue.500"
1866
+ cursor="pointer"
1867
+ onClick={() =>
1868
+ window.open(
1869
+ `https://layerzeroscan.com/tx/${txHash}`,
1870
+ "_blank",
1871
+ )
1872
+ }
1873
+ >
1874
+ View on LayerZero
1875
+ </Text>
1876
+ </Table.Cell>
1877
+ </Table.Row>
1878
+ )}
1879
+ {isCrosschain && destinationTxHash && (
1880
+ <Table.Row>
1881
+ <Table.Cell>Destination TX</Table.Cell>
1882
+ <Table.Cell
1883
+ display="flex"
1884
+ textAlign="end"
1885
+ justifyContent="end"
1886
+ >
1887
+ <Text
1888
+ fontSize="sm"
1889
+ color="blue.500"
1890
+ cursor="pointer"
1891
+ onClick={() => {
1892
+ const explorer =
1893
+ CHAINS_ETHERSCAN[
1894
+ chainIdOut as keyof typeof CHAINS_ETHERSCAN
1895
+ ] || "https://etherscan.io";
1896
+ window.open(
1897
+ `${explorer}/tx/${destinationTxHash}`,
1898
+ "_blank",
1899
+ );
1900
+ }}
1901
+ >
1902
+ View on Explorer
1903
+ </Text>
1904
+ </Table.Cell>
1905
+ </Table.Row>
1906
+ )}
1907
+ {isCrosschain && phase === "bridge" && (
1908
+ <Table.Row>
1909
+ <Table.Cell>Bridge Progress</Table.Cell>
1910
+ <Table.Cell
1911
+ display="flex"
1912
+ textAlign="end"
1913
+ justifyContent="end"
1914
+ >
1915
+ <Text>({lzStatus.step}/4)</Text>
1916
+ </Table.Cell>
1917
+ </Table.Row>
1918
+ )}
1919
+ {isCrosschain && refundDetails && (
1920
+ <Table.Row>
1921
+ <Table.Cell>Refund</Table.Cell>
1922
+ <Table.Cell
1923
+ display="flex"
1924
+ textAlign="end"
1925
+ justifyContent="end"
1926
+ >
1927
+ <Text fontSize="sm" color="orange.500">
1928
+ Funds refunded to smart account
1929
+ </Text>
1930
+ </Table.Cell>
1931
+ </Table.Row>
1932
+ )}
1933
+ </Table.Body>
1934
+ </Table.Root>
1935
+
1936
+ <QuoteParameters />
1937
+
1938
+ <TransactionDetailRow />
1939
+
1940
+ {(phase === "completed" || phase === "failed") && (
1941
+ <Button
1942
+ onClick={() => setStep(WithdrawalStep.CheckSessionKey)}
1943
+ visual="solid"
1944
+ >
1945
+ {phase === "completed" ? "New Deposit" : "Retry Deposit"}
1946
+ </Button>
1947
+ )}
1948
+ </BodyWrapper>
1949
+ );
1950
+ };
1951
+
820
1952
  const ExchangeFlow = ({
821
1953
  setFlow,
1954
+ initialStep = WithdrawalStep.ChooseExchange,
822
1955
  }: {
823
- setFlow: (flow: string) => void;
1956
+ setFlow: (string) => void;
1957
+ initialStep?: WithdrawalStep;
824
1958
  }) => {
825
1959
  const { handleClose, enforceFlow } = useContext(CheckoutContext);
826
- const [currentStep, setCurrentStep] = useState<WithdrawalStep>(
827
- WithdrawalStep.ChooseExchange,
828
- );
1960
+ const [currentStep, setCurrentStep] = useState(initialStep);
829
1961
  const [selectedToken, setSelectedToken] = useState<MatchedToken | null>(
830
1962
  null,
831
1963
  );
832
1964
  const [userOp, setUserOp] = useState<any | null>(null);
833
-
1965
+ const setSelectedIntegration = useAppStore(
1966
+ (state) => state.setSelectedIntegration,
1967
+ );
834
1968
  const selectedIntegration = useAppStore((s) => s.selectedIntegration);
835
1969
 
836
1970
  useEffect(() => {
837
- return () => useAppStore.getState().setSelectedIntegration(null);
1971
+ return () => setSelectedIntegration(null);
838
1972
  }, []);
839
1973
 
840
1974
  const currentStepComponent = (() => {
@@ -850,19 +1984,29 @@ const ExchangeFlow = ({
850
1984
  onTokenSelect={setSelectedToken}
851
1985
  />
852
1986
  );
1987
+ case WithdrawalStep.ChooseBalanceAsset:
1988
+ return (
1989
+ <ChooseDelayedBalance
1990
+ setStep={setCurrentStep}
1991
+ onTokenSelect={setSelectedToken}
1992
+ />
1993
+ );
853
1994
  case WithdrawalStep.ChooseAmount:
854
1995
  return (
855
- <SharedChooseAmountStep
1996
+ <ChooseAmountStep
856
1997
  setStep={setCurrentStep}
857
- nextStep={WithdrawalStep.SignUserOp}
858
1998
  selectedToken={selectedToken}
859
- mode="cex"
860
1999
  />
861
2000
  );
862
2001
  case WithdrawalStep.SignUserOp:
863
2002
  return (
864
- <SharedSignUserOpStep
865
- nextStep={WithdrawalStep.InitiateWithdrawal}
2003
+ <SignUserOpStep
2004
+ nextStep={
2005
+ // skip withdrawal if use existing balance
2006
+ selectedToken.holding
2007
+ ? WithdrawalStep.InitiateWithdrawal
2008
+ : WithdrawalStep.TrackUserOp
2009
+ }
866
2010
  setStep={setCurrentStep}
867
2011
  setUserOp={setUserOp}
868
2012
  />
@@ -878,10 +2022,9 @@ const ExchangeFlow = ({
878
2022
  case WithdrawalStep.TrackUserOp:
879
2023
  return (
880
2024
  <TrackUserOpStep
2025
+ selectedToken={selectedToken}
881
2026
  userOp={userOp}
882
- onReset={() =>
883
- setCurrentStep(WithdrawalStep.CheckSessionKey)
884
- }
2027
+ setStep={setCurrentStep}
885
2028
  />
886
2029
  );
887
2030
  default:
@@ -895,15 +2038,21 @@ const ExchangeFlow = ({
895
2038
  <HeaderWrapper>
896
2039
  {!(
897
2040
  enforceFlow &&
898
- currentStep === WithdrawalStep.ChooseExchange
2041
+ (currentStep === WithdrawalStep.ChooseExchange ||
2042
+ currentStep === WithdrawalStep.ChooseBalanceAsset)
899
2043
  ) && (
900
2044
  <IconButton
901
2045
  minWidth={"16px"}
902
2046
  minHeight={"16px"}
903
2047
  maxWidth={"16px"}
904
2048
  onClick={() => {
905
- const previousStep =
906
- withdrawalPreviousStep[currentStep];
2049
+ const previousStep = (
2050
+ isDelayedBalanceUsed(
2051
+ selectedIntegration?.type,
2052
+ )
2053
+ ? balancePreviousStep
2054
+ : withdrawalPreviousStep
2055
+ )[currentStep];
907
2056
  if (previousStep !== undefined) {
908
2057
  setCurrentStep(previousStep);
909
2058
  } else {
@@ -913,7 +2062,7 @@ const ExchangeFlow = ({
913
2062
  >
914
2063
  <Icon
915
2064
  as={ChevronLeft}
916
- color="fg.muted"
2065
+ color="gray"
917
2066
  width={"16px"}
918
2067
  height={"16px"}
919
2068
  />
@@ -942,7 +2091,7 @@ const ExchangeFlow = ({
942
2091
  >
943
2092
  <Icon
944
2093
  as={X}
945
- color="fg.muted"
2094
+ color="gray"
946
2095
  width={"16px"}
947
2096
  height={"16px"}
948
2097
  />
@@ -951,9 +2100,33 @@ const ExchangeFlow = ({
951
2100
  </HeaderWrapper>
952
2101
  </Modal.Header>
953
2102
  <Modal.Body>
954
- <AnimatedStep key={currentStep}>
955
- {currentStepComponent}
956
- </AnimatedStep>
2103
+ {/*{wrongChain ? (*/}
2104
+ {/* <BodyWrapper>*/}
2105
+ {/* <Box*/}
2106
+ {/* display="flex"*/}
2107
+ {/* flexDirection="column"*/}
2108
+ {/* alignItems="center"*/}
2109
+ {/* gap="16px"*/}
2110
+ {/* textAlign="center"*/}
2111
+ {/* >*/}
2112
+ {/* <Text fontSize="16px" fontWeight="600">*/}
2113
+ {/* Wrong Network*/}
2114
+ {/* </Text>*/}
2115
+ {/* <Text fontSize="14px" color="fg.muted">*/}
2116
+ {/* Please switch to {getChainName(chainIdOut)} to*/}
2117
+ {/* continue with your Binance withdrawal.*/}
2118
+ {/* </Text>*/}
2119
+ {/* <Button*/}
2120
+ {/* onClick={() => {*/}
2121
+ {/* switchChain({ chainId: chainIdOut });*/}
2122
+ {/* }}*/}
2123
+ {/* >*/}
2124
+ {/* Switch to {getChainName(chainIdOut)}*/}
2125
+ {/* </Button>*/}
2126
+ {/* </Box>*/}
2127
+ {/* </BodyWrapper>*/}
2128
+ {/*) : */}
2129
+ {currentStepComponent}
957
2130
  </Modal.Body>
958
2131
  </>
959
2132
  );