@ensofinance/checkout-widget 0.0.1

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 (128) hide show
  1. package/dist/checkout-widget.es.js +52889 -0
  2. package/dist/checkout-widget.es.js.map +1 -0
  3. package/dist/checkout-widget.umd.js +203 -0
  4. package/dist/checkout-widget.umd.js.map +1 -0
  5. package/dist/index.d.ts +23 -0
  6. package/enso-api.yaml +1982 -0
  7. package/orval.config.ts +25 -0
  8. package/package.json +79 -0
  9. package/src/assets/BinanceBadge.svg +4 -0
  10. package/src/assets/CoinbaseIcon.svg +4 -0
  11. package/src/assets/USD Coin (USDC).svg +5 -0
  12. package/src/assets/avecIcon.svg +5 -0
  13. package/src/assets/base.webp +0 -0
  14. package/src/assets/depositIcon.svg +6 -0
  15. package/src/assets/eth.webp +0 -0
  16. package/src/assets/ethMainnetIcon.svg +10 -0
  17. package/src/assets/fail.svg +5 -0
  18. package/src/assets/kraken.png +0 -0
  19. package/src/assets/logo.svg +10 -0
  20. package/src/assets/mastercard.png +0 -0
  21. package/src/assets/metamask.png +0 -0
  22. package/src/assets/rabby.png +0 -0
  23. package/src/assets/success.svg +4 -0
  24. package/src/assets/usdc.webp +0 -0
  25. package/src/assets/usdt.webp +0 -0
  26. package/src/assets/visa.png +0 -0
  27. package/src/assets/visa.webp +0 -0
  28. package/src/components/BridgeFee.tsx +58 -0
  29. package/src/components/ChakraProvider.tsx +372 -0
  30. package/src/components/Checkout.tsx +127 -0
  31. package/src/components/CheckoutModal.tsx +22 -0
  32. package/src/components/CircleTimer.tsx +66 -0
  33. package/src/components/CurrencySwapDisplay.tsx +153 -0
  34. package/src/components/DepositProcessing.tsx +116 -0
  35. package/src/components/ExchangeConfirmSecurity.tsx +110 -0
  36. package/src/components/QuoteParameters.tsx +341 -0
  37. package/src/components/TransactionDetailRow.tsx +124 -0
  38. package/src/components/cards/AssetCard.tsx +167 -0
  39. package/src/components/cards/ExchangeCard.tsx +53 -0
  40. package/src/components/cards/OptionCard.tsx +59 -0
  41. package/src/components/cards/WalletCard.tsx +99 -0
  42. package/src/components/cards/index.ts +6 -0
  43. package/src/components/modal.tsx +83 -0
  44. package/src/components/steps/ExchangeFlow.tsx +1402 -0
  45. package/src/components/steps/InitialStep.tsx +169 -0
  46. package/src/components/steps/QuoteStep.tsx +121 -0
  47. package/src/components/steps/WalletAmountStep.tsx +258 -0
  48. package/src/components/steps/WalletConfirmStep.tsx +404 -0
  49. package/src/components/steps/WalletTokenStep.tsx +128 -0
  50. package/src/components/ui/index.tsx +394 -0
  51. package/src/components/ui/styled.tsx +85 -0
  52. package/src/components/ui/toaster.tsx +43 -0
  53. package/src/components/ui/tooltip.tsx +46 -0
  54. package/src/enso-api/api.ts +173 -0
  55. package/src/enso-api/custom-instance.ts +35 -0
  56. package/src/enso-api/index.ts +5119 -0
  57. package/src/enso-api/model/action.ts +17 -0
  58. package/src/enso-api/model/actionAction.ts +52 -0
  59. package/src/enso-api/model/actionInputs.ts +12 -0
  60. package/src/enso-api/model/actionToBundle.ts +19 -0
  61. package/src/enso-api/model/actionToBundleAction.ts +53 -0
  62. package/src/enso-api/model/actionToBundleArgs.ts +12 -0
  63. package/src/enso-api/model/bundleControllerBundleShortcutTransactionParams.ts +53 -0
  64. package/src/enso-api/model/bundleControllerBundleShortcutTransactionRoutingStrategy.ts +23 -0
  65. package/src/enso-api/model/bundleShortcutTransaction.ts +35 -0
  66. package/src/enso-api/model/bundleShortcutTransactionAmountsOut.ts +15 -0
  67. package/src/enso-api/model/bundleShortcutTransactionFeeAmount.ts +12 -0
  68. package/src/enso-api/model/connectedNetwork.ts +16 -0
  69. package/src/enso-api/model/hop.ts +24 -0
  70. package/src/enso-api/model/hopArgs.ts +12 -0
  71. package/src/enso-api/model/index.ts +70 -0
  72. package/src/enso-api/model/iporControllerIporShortcutTransactionParams.ts +21 -0
  73. package/src/enso-api/model/iporShortcutInput.ts +33 -0
  74. package/src/enso-api/model/iporShortcutTransaction.ts +22 -0
  75. package/src/enso-api/model/lZDestinationTokenData.ts +19 -0
  76. package/src/enso-api/model/lZPoolLookupResponse.ts +26 -0
  77. package/src/enso-api/model/layerZeroControllerGetPoolAddressParams.ts +29 -0
  78. package/src/enso-api/model/network.ts +15 -0
  79. package/src/enso-api/model/networksControllerNetworksParams.ts +21 -0
  80. package/src/enso-api/model/nonTokenizedControllerTokens200.ts +15 -0
  81. package/src/enso-api/model/nonTokenizedControllerTokens200AllOf.ts +16 -0
  82. package/src/enso-api/model/nonTokenizedControllerTokensParams.ts +41 -0
  83. package/src/enso-api/model/nonTokenizedModel.ts +27 -0
  84. package/src/enso-api/model/nontokenizedControllerRouteNontokenizedShorcutTransactionParams.ts +64 -0
  85. package/src/enso-api/model/nontokenizedControllerRouteNontokenizedShorcutTransactionRoutingStrategy.ts +22 -0
  86. package/src/enso-api/model/paginatedResult.ts +16 -0
  87. package/src/enso-api/model/paginationMeta.ts +27 -0
  88. package/src/enso-api/model/positionModel.ts +77 -0
  89. package/src/enso-api/model/price.ts +20 -0
  90. package/src/enso-api/model/pricesControllerGetPricesParams.ts +17 -0
  91. package/src/enso-api/model/project.ts +15 -0
  92. package/src/enso-api/model/protocol.ts +15 -0
  93. package/src/enso-api/model/protocolModel.ts +26 -0
  94. package/src/enso-api/model/protocolsControllerFindAllParams.ts +21 -0
  95. package/src/enso-api/model/routeShortcutTransaction.ts +33 -0
  96. package/src/enso-api/model/routeShortcutVariableInputs.ts +68 -0
  97. package/src/enso-api/model/routeShortcutVariableInputsRoutingStrategy.ts +27 -0
  98. package/src/enso-api/model/routeShortcutVariableInputsVariableEstimates.ts +14 -0
  99. package/src/enso-api/model/routerControllerRouteShortcutTransactionParams.ts +91 -0
  100. package/src/enso-api/model/routerControllerRouteShortcutTransactionRoutingStrategy.ts +23 -0
  101. package/src/enso-api/model/standard.ts +18 -0
  102. package/src/enso-api/model/standardAction.ts +20 -0
  103. package/src/enso-api/model/standardActionAction.ts +53 -0
  104. package/src/enso-api/model/tokenModel.ts +36 -0
  105. package/src/enso-api/model/tokensControllerTokens200.ts +15 -0
  106. package/src/enso-api/model/tokensControllerTokens200AllOf.ts +16 -0
  107. package/src/enso-api/model/tokensControllerTokensParams.ts +91 -0
  108. package/src/enso-api/model/tokensControllerTokensType.ts +19 -0
  109. package/src/enso-api/model/transaction.ts +17 -0
  110. package/src/enso-api/model/userOperation.ts +28 -0
  111. package/src/enso-api/model/walletApproveTransaction.ts +24 -0
  112. package/src/enso-api/model/walletApproveTransactionTx.ts +15 -0
  113. package/src/enso-api/model/walletBalance.ts +29 -0
  114. package/src/enso-api/model/walletControllerCreateApproveTransactionParams.ts +35 -0
  115. package/src/enso-api/model/walletControllerCreateApproveTransactionRoutingStrategy.ts +23 -0
  116. package/src/enso-api/model/walletControllerWalletBalancesParams.ts +25 -0
  117. package/src/index.ts +17 -0
  118. package/src/store.ts +68 -0
  119. package/src/types/assets.d.ts +29 -0
  120. package/src/types/index.ts +21 -0
  121. package/src/util/common.tsx +324 -0
  122. package/src/util/constants.tsx +213 -0
  123. package/src/util/enso-hooks.tsx +203 -0
  124. package/src/util/index.tsx +68 -0
  125. package/src/util/tx-tracker.tsx +301 -0
  126. package/src/util/wallet.tsx +258 -0
  127. package/tsconfig.json +13 -0
  128. package/vite.config.ts +51 -0
@@ -0,0 +1,1402 @@
1
+ import {
2
+ Center,
3
+ Spinner,
4
+ Box,
5
+ Icon,
6
+ Text,
7
+ Flex,
8
+ Skeleton,
9
+ Image,
10
+ Table,
11
+ } from "@chakra-ui/react";
12
+ import { ChevronLeft, X, ArrowDownUpIcon } from "lucide-react";
13
+ import { useContext, useEffect, useMemo, useState } from "react";
14
+ import { IconButton, Button, Tab, Input, Tooltip } from "../ui";
15
+ import { useAccount, useSignMessage, useChainId, useSwitchChain } from "wagmi";
16
+ import { getUserOperationHash } from "viem/account-abstraction";
17
+ import {
18
+ BodyWrapper,
19
+ HeaderDescription,
20
+ HeaderTitle,
21
+ HeaderWrapper,
22
+ ListWrapper,
23
+ } from "../ui/styled";
24
+ import { CheckoutContext } from "../Checkout";
25
+ import Modal from "../modal";
26
+ import { createLink, IntegrationAccessToken } from "@meshconnect/web-link-sdk";
27
+ import { useAppStore } from "@/store";
28
+ import { AssetCard } from "../cards";
29
+ import { formatNumber, formatUSD } from "@/util";
30
+ import { useTokenFromListBySymbols, getChainName } from "@/util/common";
31
+ import { MIN_AMOUNT } from "@/util/constants";
32
+ import { useAppDetails, useRouteData } from "@/util/enso-hooks";
33
+ import QuoteParameters from "../QuoteParameters";
34
+ import { TransactionDetailRow } from "../TransactionDetailRow";
35
+ // @ts-ignore
36
+ import SuccessIcon from "../../assets/success.svg";
37
+ // @ts-ignore
38
+ import FailIcon from "../../assets/fail.svg";
39
+ import { CircleTimer } from "../CircleTimer";
40
+ import { ConfirmExchangeStep } from "../ExchangeConfirmSecurity";
41
+
42
+ const ENTRY_POINT_ADDRESS: `0x${string}` =
43
+ "0x0000000071727de22e5e9d8baf0edac6f37da032";
44
+
45
+ // // Styled components
46
+ // export const BodyWrapper = chakra("div", {
47
+ // base: {
48
+ // display: "flex",
49
+ // flexDirection: "column",
50
+ // width: "100%",
51
+ // justifyContent: "center",
52
+ // alignItems: "center",
53
+ // gap: "16px",
54
+ // paddingTop: "16px",
55
+ // },
56
+ // });
57
+
58
+ // Types for Mesh Holdings API response
59
+ interface CryptocurrencyPosition {
60
+ marketValue: number;
61
+ lastPrice: number;
62
+ name: string;
63
+ symbol: string;
64
+ amount: number;
65
+ }
66
+
67
+ interface HoldingsContent {
68
+ equityPositions: any[];
69
+ cryptocurrencyPositions: CryptocurrencyPosition[];
70
+ status: string;
71
+ errorMessage: string;
72
+ displayMessage: string;
73
+ notSupportedEquityPositions: any[];
74
+ notSupportedCryptocurrencyPositions: any[];
75
+ nftPositions: any[];
76
+ optionPositions: any[];
77
+ type: string;
78
+ accountId: string;
79
+ institutionName: string;
80
+ accountName: string;
81
+ }
82
+
83
+ interface HoldingsResponse {
84
+ content: HoldingsContent;
85
+ status: string;
86
+ message: string;
87
+ errorHash: string;
88
+ errorType: string;
89
+ }
90
+
91
+ interface SupportedToken {
92
+ symbol: string;
93
+ name: string;
94
+ networkId: string;
95
+ chainId: number;
96
+ integrationNetworks: any[];
97
+ }
98
+
99
+ interface MatchedToken extends SupportedToken {
100
+ balance: number;
101
+ marketValue: number;
102
+ holding: CryptocurrencyPosition;
103
+ }
104
+
105
+ // const MESH_API_URL = "http://localhost:8787";
106
+ const MESH_API_URL = "https://mesh-bff.enso-checkout.workers.dev";
107
+
108
+ /*
109
+ Withdrawal steps:
110
+ 1. Check if session key is available
111
+ 2. Perform auth if not availble (optional)
112
+ 3. Get holdings and show token selector
113
+ 4. Select amount
114
+ 6. Get userOp signature
115
+ 7. Open transfer modal with amount and token
116
+ */
117
+
118
+ enum WithdrawalStep {
119
+ CheckSessionKey,
120
+ GetHoldings,
121
+ SelectAmount,
122
+ GetUserOpSignature,
123
+ InitiateWithdrawal,
124
+ TrackUserOp,
125
+ }
126
+ const withdrawalSteps = [
127
+ WithdrawalStep.CheckSessionKey,
128
+ WithdrawalStep.GetHoldings,
129
+ WithdrawalStep.SelectAmount,
130
+ WithdrawalStep.GetUserOpSignature,
131
+ WithdrawalStep.InitiateWithdrawal,
132
+ ];
133
+
134
+ const BINANCE_INTEGRATION_ID = "9226e5c2-ebc3-4fdd-94f6-ed52cdce1420";
135
+
136
+ // Mesh network IDs for EVM chains (from Mesh networks API)
137
+ const MESH_NETWORK_IDS: { [chainId: number]: string } = {
138
+ 1: "e3c7fdd8-b1fc-4e51-85ae-bb276e075611", // Ethereum
139
+ 8453: "aa883b03-120d-477c-a588-37c2afd3ca71", // Base
140
+ 42161: "a34f2431-0ddd-4de4-bc22-4a8143287aeb", // Arbitrum
141
+ 137: "7436e9d0-ba42-4d2b-b4c0-8e4e606b2c12", // Polygon
142
+ 10: "18fa36b0-88a8-43ca-83db-9a874e0a2288", // Optimism
143
+ 43114: "bad16371-c22a-4bf4-a311-274d046cd760", // Avalanche C-Chain
144
+ 56: "ed0ebeec-b166-4c8b-8574-cb078f7af8cf", // BSC
145
+ 146: "385f0b3a-8471-4b8f-884f-c4f4496f1603", // Sonic
146
+ // 81457: "0c17e03f-77fa-4644-b84c-eb247af8c4c1", // Blast
147
+ // 11155111: "03b2d786-7092-4a6a-9737-d6013e21819b", // Sepolia (testnet)
148
+ };
149
+
150
+ const getNetworkId = (chainId: number): string => {
151
+ return MESH_NETWORK_IDS[chainId] || MESH_NETWORK_IDS[8453]; // Default to Base
152
+ };
153
+
154
+ const CheckSessionKeyStep = ({
155
+ setStep,
156
+ }: {
157
+ setStep: (step: WithdrawalStep) => void;
158
+ }) => {
159
+ const { chainIdOut, setMeshAccessToken, setSessionId } = useAppStore();
160
+ const { address } = useAccount();
161
+ const storageKey = useStorageKey();
162
+ const [showConfirmation, setShowConfirmation] = useState(false);
163
+
164
+ useEffect(() => {
165
+ // If connection is persisted, skip fetching a new link token
166
+ const saved = localStorage.getItem(storageKey);
167
+ // On load: check for persisted connection and hydrate state
168
+ if (saved) {
169
+ try {
170
+ const parsed = JSON.parse(saved);
171
+ if (parsed?.accessToken && parsed?.sessionId) {
172
+ setMeshAccessToken(parsed.accessToken);
173
+ setSessionId(parsed.sessionId);
174
+ setStep(WithdrawalStep.GetHoldings);
175
+ return;
176
+ }
177
+ } catch (e) {
178
+ // ignore malformed storage
179
+ console.error("Failed to parse saved Mesh connection", e);
180
+ }
181
+ }
182
+
183
+ // Show confirmation instead of auto-connecting
184
+ setShowConfirmation(true);
185
+ }, [chainIdOut, address, storageKey]);
186
+
187
+ const handleConfirmAuth = () => {
188
+ fetch(`${MESH_API_URL}/linktoken`, {
189
+ method: "POST",
190
+ headers: {
191
+ "Content-Type": "application/json",
192
+ },
193
+ body: JSON.stringify({
194
+ userId: address,
195
+ integrationId: BINANCE_INTEGRATION_ID,
196
+ address,
197
+ }),
198
+ })
199
+ .then((response) => response.json())
200
+ .then((response) => {
201
+ setSessionId(response.content.sessionId);
202
+
203
+ const meshLink = createLink({
204
+ clientId: address,
205
+ onIntegrationConnected: (payload) => {
206
+ console.log("onIntegrationConnected", payload);
207
+ setMeshAccessToken(payload.accessToken);
208
+ setStep(WithdrawalStep.GetHoldings);
209
+ // Persist access token and session id for future reloads
210
+ try {
211
+ localStorage.setItem(
212
+ storageKey,
213
+ JSON.stringify({
214
+ accessToken: payload.accessToken,
215
+ sessionId: response.content.sessionId,
216
+ timestamp: Date.now(),
217
+ }),
218
+ );
219
+ meshLink.closeLink();
220
+ } catch (e) {
221
+ console.error(
222
+ "Failed to persist Mesh connection",
223
+ e,
224
+ );
225
+ }
226
+ },
227
+ onExit: (error) => {
228
+ debugger;
229
+ },
230
+ onTransferFinished: (transferData) => {
231
+ debugger;
232
+ },
233
+ onEvent: (ev) => {
234
+ console.log(ev);
235
+ },
236
+ });
237
+
238
+ meshLink.openLink(response.content.linkToken);
239
+ })
240
+ .catch((err) => console.error(err));
241
+ };
242
+
243
+ if (showConfirmation) {
244
+ return <ConfirmExchangeStep onConfirm={handleConfirmAuth} />;
245
+ }
246
+
247
+ return <Spinner m={5} />;
248
+ };
249
+
250
+ // Generate a unique device ID to use as user id for Mesh
251
+ const useStorageKey = () => {
252
+ return useMemo(() => {
253
+ const deviceIdKey = "meshDeviceId";
254
+ let deviceId = localStorage.getItem(deviceIdKey);
255
+
256
+ if (!deviceId) {
257
+ deviceId = `device_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
258
+ localStorage.setItem(deviceIdKey, deviceId);
259
+ }
260
+
261
+ return deviceId;
262
+ }, []);
263
+ };
264
+
265
+ const ChooseAssetStep = ({
266
+ setStep,
267
+ onTokenSelect,
268
+ }: {
269
+ setStep: (step: WithdrawalStep) => void;
270
+ onTokenSelect: (token: MatchedToken) => void;
271
+ }) => {
272
+ const [holdings, setHoldings] = useState<CryptocurrencyPosition[]>([]);
273
+ const [supportedTokens, setSupportedTokens] = useState<SupportedToken[]>(
274
+ [],
275
+ );
276
+ const [matchedTokens, setMatchedTokens] = useState<MatchedToken[]>([]);
277
+ const [selectedTokenSymbol, setSelectedTokenSymbol] = useState<
278
+ string | null
279
+ >(null);
280
+ const [loading, setLoading] = useState(true);
281
+ const [error, setError] = useState<string | null>(null);
282
+ const { address } = useAccount();
283
+ const {
284
+ chainIdOut,
285
+ meshAccessToken,
286
+ sessionId,
287
+ setMeshAccessToken,
288
+ setSessionId,
289
+ setTokenIn,
290
+ chainIdIn,
291
+ } = useAppStore();
292
+
293
+ const storageKey = useStorageKey();
294
+ useEffect(() => {
295
+ const fetchData = async () => {
296
+ try {
297
+ // Fetch holdings from Binance
298
+ const holdingsResponse = await fetch(
299
+ `${MESH_API_URL}/holdings`,
300
+ {
301
+ method: "POST",
302
+ headers: {
303
+ "Content-Type": "application/json",
304
+ "x-session-id": sessionId,
305
+ },
306
+ body: JSON.stringify({
307
+ authToken:
308
+ meshAccessToken.accountTokens[0].accessToken,
309
+ brokerType: "binanceInternationalDirect",
310
+ }),
311
+ },
312
+ );
313
+
314
+ const holdingsData: HoldingsResponse =
315
+ await holdingsResponse.json();
316
+ console.log("Holdings data:", holdingsData);
317
+
318
+ if (
319
+ holdingsData.status === "ok" &&
320
+ holdingsData.content?.cryptocurrencyPositions
321
+ ) {
322
+ setHoldings(holdingsData.content.cryptocurrencyPositions);
323
+ } else {
324
+ if (holdingsData.errorType === "invalidIntegrationToken") {
325
+ console.log("Invalid integration token");
326
+ setStep(WithdrawalStep.CheckSessionKey);
327
+ setMeshAccessToken(null);
328
+ setSessionId(null);
329
+ localStorage.removeItem(storageKey);
330
+ }
331
+ throw new Error(
332
+ holdingsData.message || "Failed to fetch holdings",
333
+ );
334
+ }
335
+
336
+ // Fetch supported tokens for current chain
337
+ const tokensResponse = await fetch(
338
+ `${MESH_API_URL}/tokens?chainId=${chainIdIn}`,
339
+ );
340
+ const tokensData = await tokensResponse.json();
341
+ console.log("Tokens data:", tokensData);
342
+
343
+ if (
344
+ tokensData.status === "success" &&
345
+ tokensData.content?.tokens
346
+ ) {
347
+ setSupportedTokens(tokensData.content.tokens);
348
+
349
+ // Match holdings with supported tokens
350
+ const matched = tokensData.content.tokens
351
+ .map((token: SupportedToken) => {
352
+ const holding =
353
+ holdingsData.content.cryptocurrencyPositions.find(
354
+ (h: CryptocurrencyPosition) =>
355
+ h.symbol === token.symbol,
356
+ );
357
+ if (holding) {
358
+ return {
359
+ ...token,
360
+ balance: holding.amount,
361
+ marketValue: holding.marketValue,
362
+ holding: holding,
363
+ } as MatchedToken;
364
+ }
365
+ return null;
366
+ })
367
+ .filter(
368
+ (token): token is MatchedToken =>
369
+ token !== null && token.marketValue > 5,
370
+ )
371
+ .sort((a, b) => b.marketValue - a.marketValue);
372
+
373
+ console.log("Matched tokens with balances:", matched);
374
+ setMatchedTokens(matched);
375
+ } else {
376
+ throw new Error(
377
+ tokensData.message ||
378
+ "Failed to fetch supported tokens",
379
+ );
380
+ }
381
+ } catch (err) {
382
+ debugger;
383
+ console.error("Error fetching data:", err);
384
+ setError(
385
+ err instanceof Error ? err.message : "Failed to fetch data",
386
+ );
387
+ } finally {
388
+ setLoading(false);
389
+ }
390
+ };
391
+
392
+ if (meshAccessToken && sessionId && chainIdOut) {
393
+ fetchData();
394
+ }
395
+ }, [address, chainIdOut, meshAccessToken, sessionId]);
396
+
397
+ const geckoTokens = useTokenFromListBySymbols(
398
+ matchedTokens.map((token) => token.symbol),
399
+ chainIdIn,
400
+ );
401
+
402
+ console.log("geckoTokens", geckoTokens);
403
+
404
+ if (loading)
405
+ return (
406
+ <Center>
407
+ <Spinner m={5} />
408
+ </Center>
409
+ );
410
+ if (error)
411
+ return (
412
+ <Box p={5} color="red.500">
413
+ Error: {error}
414
+ </Box>
415
+ );
416
+
417
+ return (
418
+ <BodyWrapper>
419
+ <Box mb={4} width="100%" textAlign="left">
420
+ <HeaderTitle>Your Holdings</HeaderTitle>
421
+ <HeaderDescription>
422
+ Found {matchedTokens.length} tokens with balances
423
+ </HeaderDescription>
424
+ </Box>
425
+ <Box overflowY={"scroll"} maxH={"400px"}>
426
+ <ListWrapper>
427
+ {matchedTokens.map((token, index) => (
428
+ <AssetCard
429
+ key={`${token.symbol}-${index}`}
430
+ chainId={chainIdOut || 1}
431
+ icon={geckoTokens?.[index]?.logoURI?.replace(
432
+ "/thumb/",
433
+ "/large/",
434
+ )}
435
+ title={token.name}
436
+ balance={`${formatNumber(token.balance)} ${token.symbol}`}
437
+ usdBalance={formatUSD(token.marketValue || 0)}
438
+ tag=""
439
+ loading={false}
440
+ selected={selectedTokenSymbol === token.symbol}
441
+ onClick={() => {
442
+ setSelectedTokenSymbol(token.symbol);
443
+ onTokenSelect(token);
444
+ setTokenIn(geckoTokens?.[index]?.address);
445
+ }}
446
+ />
447
+ ))}
448
+ </ListWrapper>
449
+ </Box>
450
+ {matchedTokens.length === 0 && (
451
+ <Box textAlign="center" color="fg.subtle" py={8}>
452
+ No tokens with balances found for this chain
453
+ </Box>
454
+ )}
455
+ {
456
+ <Button
457
+ disabled={!selectedTokenSymbol}
458
+ onClick={() => {
459
+ setStep(WithdrawalStep.SelectAmount);
460
+ }}
461
+ >
462
+ Continue
463
+ </Button>
464
+ }
465
+ </BodyWrapper>
466
+ );
467
+ };
468
+
469
+ const ChooseAmountStep = ({
470
+ setStep,
471
+ selectedToken,
472
+ }: {
473
+ setStep: (step: WithdrawalStep) => void;
474
+ selectedToken: MatchedToken | null;
475
+ }) => {
476
+ const [amount, setAmount] = useState<string>("");
477
+ const [inputMode, setInputMode] = useState<"usd" | "token">("usd");
478
+ const [usdValue, setUsdValue] = useState<string>("");
479
+ const { setAmountIn } = useAppStore();
480
+ const { tokenInData, tokenIn } = useAppDetails();
481
+
482
+ // Set max value on load
483
+ useEffect(() => {
484
+ if (selectedToken) {
485
+ setAmount(selectedToken.balance.toString());
486
+ setUsdValue(selectedToken.marketValue.toFixed(2));
487
+ }
488
+ }, [selectedToken]);
489
+
490
+ useEffect(() => {
491
+ console.log("tokenIn", tokenIn);
492
+ if (tokenInData?.decimals)
493
+ setAmountIn(
494
+ (Number(amount) * 10 ** tokenInData?.decimals).toFixed(),
495
+ );
496
+ }, [amount, tokenInData?.decimals]);
497
+
498
+ // Handle percentage selection
499
+ const handlePercentageSelect = (percent: number) => {
500
+ if (!selectedToken) return;
501
+
502
+ const amountToSet = (selectedToken.balance * percent) / 100;
503
+ const usdAmountToSet = (selectedToken.marketValue * percent) / 100;
504
+
505
+ setAmount(amountToSet.toString());
506
+ setUsdValue(usdAmountToSet.toFixed(2));
507
+ };
508
+
509
+ // Handle input change based on current mode
510
+ const handleInputChange = (value: string) => {
511
+ if (!selectedToken) return;
512
+
513
+ if (inputMode === "usd") {
514
+ const cleanUsd = value.replace("$", "");
515
+ setUsdValue(cleanUsd);
516
+ // Calculate token amount from USD value
517
+ const tokenPrice =
518
+ selectedToken.marketValue / selectedToken.balance;
519
+ const tokenAmount = parseFloat(cleanUsd) / tokenPrice;
520
+ setAmount(tokenAmount.toString());
521
+ } else {
522
+ setAmount(value);
523
+ // Calculate USD value from token amount
524
+ const tokenPrice =
525
+ selectedToken.marketValue / selectedToken.balance;
526
+ const usdAmount = parseFloat(value) * tokenPrice;
527
+ setUsdValue(usdAmount.toFixed(2));
528
+ }
529
+ };
530
+
531
+ // Toggle between USD and token input modes
532
+ const handleToggleMode = () => {
533
+ setInputMode(inputMode === "usd" ? "token" : "usd");
534
+ };
535
+
536
+ // Get input placeholder and display value
537
+ const getInputDisplay = () => {
538
+ if (inputMode === "usd") {
539
+ return {
540
+ placeholder: "$10.00",
541
+ displayValue: usdValue ? `$${usdValue}` : "",
542
+ equivalentValue: amount
543
+ ? `${formatNumber(parseFloat(amount))} ${selectedToken?.symbol}`
544
+ : "—",
545
+ };
546
+ } else {
547
+ return {
548
+ placeholder: "0.00",
549
+ displayValue: amount,
550
+ equivalentValue: usdValue ? `$${usdValue}` : "—",
551
+ };
552
+ }
553
+ };
554
+
555
+ const { placeholder, displayValue, equivalentValue } = getInputDisplay();
556
+ const notEnoughBalance = selectedToken
557
+ ? parseFloat(amount) > selectedToken.balance
558
+ : true;
559
+
560
+ // Limits validation logic
561
+ const currentUsdValue = parseFloat(usdValue);
562
+ const minAmountForToken = selectedToken
563
+ ? MIN_AMOUNT[selectedToken.symbol as keyof typeof MIN_AMOUNT]
564
+ : 0;
565
+ const maxUsdAmount = selectedToken ? selectedToken.marketValue - 5 : 0;
566
+
567
+ const isBelowMinAmount =
568
+ selectedToken &&
569
+ currentUsdValue > 0 &&
570
+ minAmountForToken &&
571
+ currentUsdValue < minAmountForToken;
572
+ const isAboveMaxAmount =
573
+ selectedToken && currentUsdValue > 0 && currentUsdValue > maxUsdAmount;
574
+
575
+ const isAmountInvalid =
576
+ isBelowMinAmount || isAboveMaxAmount || notEnoughBalance;
577
+
578
+ if (!selectedToken) {
579
+ return (
580
+ <BodyWrapper>
581
+ <Box textAlign="center" color="fg.subtle" py={8}>
582
+ No token selected
583
+ </Box>
584
+ </BodyWrapper>
585
+ );
586
+ }
587
+
588
+ return (
589
+ <BodyWrapper>
590
+ <Box mb={4} width="100%" textAlign="left">
591
+ <HeaderTitle>Enter Amount</HeaderTitle>
592
+ <HeaderDescription>
593
+ Available: {formatNumber(selectedToken.balance)}{" "}
594
+ {selectedToken.symbol} ($
595
+ {formatUSD(selectedToken.marketValue)})
596
+ </HeaderDescription>
597
+ </Box>
598
+
599
+ <Box
600
+ display={"flex"}
601
+ flexDirection={"column"}
602
+ gap={"8px"}
603
+ width="100%"
604
+ >
605
+ <Box
606
+ display={"flex"}
607
+ flexDirection={"column"}
608
+ gap={"8px"}
609
+ alignItems={"center"}
610
+ padding={"25.5px"}
611
+ >
612
+ {/* Main Input */}
613
+ <Input
614
+ inputMode="decimal"
615
+ marginY={"8px"}
616
+ variant={"text"}
617
+ placeholder={placeholder}
618
+ value={displayValue}
619
+ onChange={(e) => handleInputChange(e.target.value)}
620
+ />
621
+
622
+ {/* Toggle Button and Equivalent Display */}
623
+ <Box
624
+ display={"flex"}
625
+ gap={"3"}
626
+ alignItems={"center"}
627
+ onClick={handleToggleMode}
628
+ _hover={{ background: "bg.subtle" }}
629
+ cursor={"pointer"}
630
+ borderRadius={"lg"}
631
+ px={"3"}
632
+ >
633
+ <IconButton
634
+ minWidth={"24px"}
635
+ minHeight={"24px"}
636
+ maxWidth={"24px"}
637
+ background={"transparent"}
638
+ >
639
+ <Icon
640
+ as={ArrowDownUpIcon}
641
+ color="gray"
642
+ width={"16px"}
643
+ height={"16px"}
644
+ />
645
+ </IconButton>
646
+
647
+ {/* Small equivalent value display */}
648
+ <Text fontSize="sm" color="fg.muted">
649
+ {equivalentValue}
650
+ </Text>
651
+ </Box>
652
+ </Box>
653
+
654
+ <Box
655
+ display={"flex"}
656
+ gap={"4px"}
657
+ justifyContent={"center"}
658
+ paddingBottom={"35px"}
659
+ >
660
+ {[25, 50, 75, 100].map((percent) => (
661
+ <Tab
662
+ key={percent}
663
+ onClick={() => handlePercentageSelect(percent)}
664
+ >
665
+ {percent === 100 ? "Max" : `${percent}%`}
666
+ </Tab>
667
+ ))}
668
+ </Box>
669
+ </Box>
670
+
671
+ <Tooltip
672
+ disabled={!isAmountInvalid && !!amount}
673
+ content={
674
+ !amount
675
+ ? "Please enter an amount"
676
+ : isBelowMinAmount
677
+ ? `Minimum amount is $${minAmountForToken} USD`
678
+ : isAboveMaxAmount
679
+ ? `Maximum amount is $${maxUsdAmount.toFixed(2)} USD (balance - $10)`
680
+ : notEnoughBalance
681
+ ? "Amount exceeds available balance"
682
+ : ""
683
+ }
684
+ showArrow
685
+ >
686
+ <Button
687
+ onClick={() =>
688
+ isAmountInvalid || !amount
689
+ ? undefined
690
+ : setStep(WithdrawalStep.GetUserOpSignature)
691
+ }
692
+ disabled={isAmountInvalid || !amount}
693
+ >
694
+ Continue
695
+ </Button>
696
+ </Tooltip>
697
+ </BodyWrapper>
698
+ );
699
+ };
700
+
701
+ const SignUserOpStep = ({
702
+ setStep,
703
+ setUserOp,
704
+ }: {
705
+ setStep: (step: WithdrawalStep) => void;
706
+ setUserOp: (userOp: any) => void;
707
+ }) => {
708
+ const { chainIdIn } = useAppDetails();
709
+ const { isLoading, usdAmountIn, routerData } = useRouteData();
710
+ const { signMessageAsync } = useSignMessage();
711
+ const { address } = useAccount();
712
+ const [isSigning, setIsSigning] = useState(false);
713
+
714
+ const handleSignUserOp = async () => {
715
+ if (!routerData || (routerData as any)?.error) {
716
+ console.error("No valid router data available");
717
+ return;
718
+ }
719
+
720
+ try {
721
+ setIsSigning(true);
722
+
723
+ // Extract userOp from routerData
724
+ const userOperation = routerData?.userOp;
725
+ if (!userOperation) {
726
+ console.error("No userOperation found in routerData");
727
+ return;
728
+ }
729
+
730
+ console.log("Signing userOperation:", userOperation);
731
+
732
+ // Use viem's getUserOperationHash function
733
+ const userOpHash = getUserOperationHash({
734
+ // @ts-ignore
735
+ userOperation,
736
+ entryPointAddress: ENTRY_POINT_ADDRESS,
737
+ entryPointVersion: "0.7",
738
+ chainId: chainIdIn,
739
+ });
740
+
741
+ console.log("Signing userOpHash:", userOpHash);
742
+
743
+ // Sign the userOperation hash directly
744
+ const signature = await signMessageAsync({
745
+ account: address as `0x${string}`,
746
+ message: { raw: userOpHash as `0x${string}` },
747
+ });
748
+
749
+ // Update userOperation with signature
750
+ const signedUserOp = {
751
+ ...userOperation,
752
+ signature,
753
+ };
754
+
755
+ console.log("signedUserOp", JSON.stringify(signedUserOp));
756
+
757
+ setUserOp(signedUserOp);
758
+ setStep(WithdrawalStep.InitiateWithdrawal);
759
+ } catch (error) {
760
+ console.error("Failed to sign userOperation:", error);
761
+ } finally {
762
+ setIsSigning(false);
763
+ }
764
+ };
765
+
766
+ return (
767
+ <BodyWrapper>
768
+ <Flex
769
+ flexDirection={"column"}
770
+ gap={"16px"}
771
+ alignItems={"center"}
772
+ width={"100%"}
773
+ >
774
+ <Skeleton
775
+ loading={isLoading}
776
+ width={isLoading ? "156px" : "auto"}
777
+ >
778
+ <Input
779
+ readOnly
780
+ marginY={"8px"}
781
+ variant={"text"}
782
+ placeholder={"$10.00"}
783
+ value={usdAmountIn}
784
+ />
785
+ </Skeleton>
786
+
787
+ <QuoteParameters />
788
+ </Flex>
789
+
790
+ <TransactionDetailRow />
791
+
792
+ <Button
793
+ disabled={
794
+ !!(routerData as any)?.message || isLoading || isSigning
795
+ }
796
+ loading={isLoading || isSigning}
797
+ onClick={handleSignUserOp}
798
+ >
799
+ {isLoading
800
+ ? "Loading quote"
801
+ : isSigning
802
+ ? "Signing..."
803
+ : "Sign Transaction"}
804
+ </Button>
805
+ </BodyWrapper>
806
+ );
807
+ };
808
+
809
+ const InitiateWithdrawalStep = ({
810
+ selectedToken,
811
+ userOp,
812
+ setStep,
813
+ }: {
814
+ selectedToken: MatchedToken | null;
815
+ userOp: any;
816
+ setStep: (step: WithdrawalStep) => void;
817
+ }) => {
818
+ const { meshAccessToken, amountIn, chainIdOut } = useAppStore();
819
+ const { address } = useAccount();
820
+ const { tokenInData } = useAppDetails();
821
+ const [isLoading, setIsLoading] = useState(true);
822
+
823
+ useEffect(() => {
824
+ if (!selectedToken || !userOp || !meshAccessToken) {
825
+ console.error("Missing required data for withdrawal initiation");
826
+ return;
827
+ }
828
+
829
+ // Convert amountIn from wei to token amount
830
+ const transferAmount = tokenInData?.decimals
831
+ ? Number(amountIn) / 10 ** tokenInData.decimals
832
+ : 0;
833
+
834
+ const fetchLinkTokenAndOpen = async () => {
835
+ try {
836
+ const toAddresses = [
837
+ {
838
+ networkId: getNetworkId(chainIdOut),
839
+ symbol: selectedToken.symbol,
840
+ address: userOp.sender,
841
+ amount: transferAmount,
842
+ },
843
+ ];
844
+ const meshData = {
845
+ restrictMultipleAccounts: true,
846
+ userId: address,
847
+ integrationId: BINANCE_INTEGRATION_ID,
848
+ transferOptions: {
849
+ toAddresses,
850
+ },
851
+ };
852
+
853
+ console.log("link request body", meshData);
854
+
855
+ const response = await fetch(`${MESH_API_URL}/linktoken`, {
856
+ method: "POST",
857
+ headers: {
858
+ "Content-Type": "application/json",
859
+ },
860
+ body: JSON.stringify({
861
+ restrictMultipleAccounts: true,
862
+ userId: address,
863
+ integrationId: BINANCE_INTEGRATION_ID,
864
+ transferOptions: {
865
+ toAddresses,
866
+ },
867
+ }),
868
+ });
869
+
870
+ const data = await response.json();
871
+ console.log("Link token response:", data);
872
+
873
+ const accessTokens: IntegrationAccessToken[] =
874
+ meshAccessToken.accountTokens.map((token) => ({
875
+ ...token,
876
+ accountId: token.account.accountId,
877
+ accountName: token.account.accountName,
878
+ brokerName: "Binance",
879
+ brokerType: "binanceInternationalDirect",
880
+ }));
881
+
882
+ console.log("accessTokens", accessTokens);
883
+
884
+ const link = createLink({
885
+ clientId: address,
886
+ accessTokens,
887
+ onIntegrationConnected: (e) => {
888
+ console.log("Integration connected", e);
889
+ },
890
+ onTransferFinished: (transferData) => {
891
+ console.log("Transfer finished:", transferData);
892
+ setIsLoading(false);
893
+ },
894
+ onExit: (error) => {
895
+ console.log("Mesh link exited:", error);
896
+ setIsLoading(false);
897
+ setStep(WithdrawalStep.GetHoldings);
898
+ },
899
+ onEvent: (ev) => {
900
+ console.log("Mesh event:", ev);
901
+ if (ev.type === "transferExecuted") {
902
+ console.log(
903
+ "Transfer executed, closing mesh link and moving to TrackUserOp step",
904
+ );
905
+ link.closeLink();
906
+ setStep(WithdrawalStep.TrackUserOp);
907
+ }
908
+ },
909
+ });
910
+
911
+ link.openLink(data.content.linkToken);
912
+ } catch (error) {
913
+ console.error("Failed to fetch link token:", error);
914
+ setIsLoading(false);
915
+ }
916
+ };
917
+
918
+ fetchLinkTokenAndOpen();
919
+ }, [selectedToken, userOp]);
920
+
921
+ return (
922
+ <BodyWrapper>
923
+ <Center>
924
+ <Text mt={4}>
925
+ {isLoading ? (
926
+ <Flex alignItems={"center"} gap={2}>
927
+ <Spinner m={5} />
928
+ <Text>Intiating Mesh</Text>
929
+ </Flex>
930
+ ) : (
931
+ "Transfer completed"
932
+ )}
933
+ </Text>
934
+ </Center>
935
+ </BodyWrapper>
936
+ );
937
+ };
938
+
939
+ const TrackUserOpStep = ({
940
+ selectedToken,
941
+ userOp,
942
+ setStep,
943
+ }: {
944
+ selectedToken: MatchedToken | null;
945
+ userOp: any;
946
+ setStep: (step: WithdrawalStep) => void;
947
+ }) => {
948
+ const { chainIdIn, tokenInData } = useAppDetails();
949
+ const { amountIn } = useAppStore();
950
+ const [operationId, setOperationId] = useState<string | null>(null);
951
+ const [status, setStatus] = useState<
952
+ "sending" | "tracking" | "completed" | "failed"
953
+ >("sending");
954
+ const [message, setMessage] = useState("Sending operation to tracker...");
955
+ const [isTimerFinished, setIsTimerFinished] = useState(false);
956
+ const [trackingInterval, setTrackingInterval] =
957
+ useState<NodeJS.Timeout | null>(null);
958
+
959
+ useEffect(() => {
960
+ const sendUserOpToTracker = async () => {
961
+ if (!selectedToken || !userOp || !tokenInData) {
962
+ console.error("Missing required data for tracking");
963
+ setStatus("failed");
964
+ setMessage("Missing required data");
965
+ return;
966
+ }
967
+
968
+ try {
969
+ const response = await fetch(
970
+ "https://alpha-scanners-dev-054573dc8549.herokuapp.com/operations",
971
+ {
972
+ method: "POST",
973
+ headers: {
974
+ "Content-Type": "application/json",
975
+ },
976
+ body: JSON.stringify({
977
+ userOperationData: {
978
+ sender: userOp.sender,
979
+ nonce: userOp.nonce,
980
+ factory: userOp.factory,
981
+ factoryData: userOp.factoryData,
982
+ callData: userOp.callData,
983
+ callGasLimit: userOp.callGasLimit,
984
+ verificationGasLimit:
985
+ userOp.verificationGasLimit,
986
+ preVerificationGas: userOp.preVerificationGas,
987
+ maxFeePerGas: userOp.maxFeePerGas,
988
+ maxPriorityFeePerGas:
989
+ userOp.maxPriorityFeePerGas,
990
+ paymaster: userOp.paymaster,
991
+ paymasterData: userOp.paymasterData,
992
+ paymasterVerificationGasLimit:
993
+ userOp.paymasterVerificationGasLimit,
994
+ paymasterPostOpGasLimit:
995
+ userOp.paymasterPostOpGasLimit,
996
+ signature: userOp.signature,
997
+ },
998
+ chainId: chainIdIn,
999
+ expectedBalance: amountIn,
1000
+ tokenAddress: tokenInData.address,
1001
+ }),
1002
+ },
1003
+ );
1004
+
1005
+ const data = await response.json();
1006
+ console.log("Operation tracking response:", data);
1007
+
1008
+ if (data.success && data.operationId) {
1009
+ setOperationId(data.operationId);
1010
+ setStatus("tracking");
1011
+ setMessage("Tracking operation progress...");
1012
+ } else {
1013
+ throw new Error(
1014
+ data.message || "Failed to send operation to tracker",
1015
+ );
1016
+ }
1017
+ } catch (error) {
1018
+ console.error("Failed to send operation to tracker:", error);
1019
+ setStatus("failed");
1020
+ setMessage("Failed to send operation to tracker");
1021
+ }
1022
+ };
1023
+
1024
+ sendUserOpToTracker();
1025
+ }, []);
1026
+
1027
+ // Track operation status
1028
+ useEffect(() => {
1029
+ if (!operationId || status !== "tracking") return;
1030
+
1031
+ const trackOperation = async () => {
1032
+ try {
1033
+ const response = await fetch(
1034
+ `https://alpha-scanners-dev-054573dc8549.herokuapp.com/operations/${operationId}/status`,
1035
+ );
1036
+ const data = await response.json();
1037
+ console.log("Operation status:", data);
1038
+
1039
+ if (data.operation?.status === "completed") {
1040
+ setStatus("completed");
1041
+ setMessage("Operation completed successfully!");
1042
+ if (trackingInterval) {
1043
+ clearInterval(trackingInterval);
1044
+ setTrackingInterval(null);
1045
+ }
1046
+ } else if (data.operation?.status === "failed") {
1047
+ setStatus("failed");
1048
+ setMessage("Operation failed");
1049
+ if (trackingInterval) {
1050
+ clearInterval(trackingInterval);
1051
+ setTrackingInterval(null);
1052
+ }
1053
+ }
1054
+ } catch (error) {
1055
+ console.error("Failed to fetch operation status:", error);
1056
+ }
1057
+ };
1058
+
1059
+ const interval = setInterval(trackOperation, 3000); // Check every 3 seconds
1060
+ setTrackingInterval(interval);
1061
+
1062
+ return () => {
1063
+ if (interval) clearInterval(interval);
1064
+ };
1065
+ }, [operationId]);
1066
+
1067
+ const handleTimerFinish = () => {
1068
+ setIsTimerFinished(true);
1069
+ };
1070
+
1071
+ const getStatusColor = () => {
1072
+ switch (status) {
1073
+ case "completed":
1074
+ return "#14AE5C";
1075
+ case "failed":
1076
+ return "#E84142";
1077
+ default:
1078
+ return "#1E171F";
1079
+ }
1080
+ };
1081
+
1082
+ const renderStatusIcon = () => {
1083
+ switch (status) {
1084
+ case "sending":
1085
+ case "tracking":
1086
+ return (
1087
+ <CircleTimer
1088
+ start={status === "tracking"}
1089
+ onFinish={handleTimerFinish}
1090
+ duration={120}
1091
+ />
1092
+ );
1093
+
1094
+ case "completed":
1095
+ return (
1096
+ <Box
1097
+ display="flex"
1098
+ flexDirection="column"
1099
+ alignItems="center"
1100
+ >
1101
+ <Image
1102
+ src={SuccessIcon}
1103
+ boxShadow="0px 0px 20px #14AE5C"
1104
+ borderRadius="90%"
1105
+ width="58px"
1106
+ height="58px"
1107
+ />
1108
+ </Box>
1109
+ );
1110
+
1111
+ case "failed":
1112
+ return (
1113
+ <Box
1114
+ display="flex"
1115
+ flexDirection="column"
1116
+ alignItems="center"
1117
+ >
1118
+ <Image
1119
+ src={FailIcon}
1120
+ boxShadow="0px 0px 20px #E84142"
1121
+ borderRadius="90%"
1122
+ width="58px"
1123
+ height="58px"
1124
+ />
1125
+ </Box>
1126
+ );
1127
+ }
1128
+ };
1129
+
1130
+ const renderProcessingText = () => {
1131
+ if (status === "tracking" && isTimerFinished) {
1132
+ return (
1133
+ <Box
1134
+ display="flex"
1135
+ flexDirection="column"
1136
+ alignItems="center"
1137
+ marginTop="16px"
1138
+ textAlign="center"
1139
+ >
1140
+ <Text
1141
+ fontSize="lg"
1142
+ fontWeight="semibold"
1143
+ color="fg"
1144
+ marginBottom="8px"
1145
+ >
1146
+ {message}
1147
+ </Text>
1148
+ <Text fontSize="sm" color="fg.muted" maxWidth="280px">
1149
+ Your operation is being processed – no action is
1150
+ required from you.
1151
+ </Text>
1152
+ </Box>
1153
+ );
1154
+ }
1155
+ return null;
1156
+ };
1157
+
1158
+ const getStatusText = () => {
1159
+ switch (status) {
1160
+ case "completed":
1161
+ return "Success";
1162
+ case "failed":
1163
+ return "Failed";
1164
+ case "tracking":
1165
+ return "Processing";
1166
+ case "sending":
1167
+ return "Sending";
1168
+ default:
1169
+ return "Unknown";
1170
+ }
1171
+ };
1172
+
1173
+ return (
1174
+ <BodyWrapper>
1175
+ <Box
1176
+ display={"flex"}
1177
+ flexDirection={"column"}
1178
+ paddingBottom={"16px"}
1179
+ alignItems={"center"}
1180
+ width={"100%"}
1181
+ >
1182
+ {renderStatusIcon()}
1183
+ {renderProcessingText()}
1184
+ </Box>
1185
+
1186
+ <Table.Root
1187
+ key={"status"}
1188
+ size="sm"
1189
+ variant={"outline"}
1190
+ width={"100%"}
1191
+ >
1192
+ <Table.Body>
1193
+ <Table.Row>
1194
+ <Table.Cell>Status</Table.Cell>
1195
+ <Table.Cell
1196
+ display="flex"
1197
+ textAlign="end"
1198
+ justifyContent="end"
1199
+ >
1200
+ <Text color={getStatusColor()}>
1201
+ {getStatusText()}
1202
+ </Text>
1203
+ </Table.Cell>
1204
+ </Table.Row>
1205
+ {operationId && (
1206
+ <Table.Row>
1207
+ <Table.Cell>Operation ID</Table.Cell>
1208
+ <Table.Cell
1209
+ display="flex"
1210
+ textAlign="end"
1211
+ justifyContent="end"
1212
+ >
1213
+ <Text fontSize="sm" color="fg.muted">
1214
+ {operationId}
1215
+ </Text>
1216
+ </Table.Cell>
1217
+ </Table.Row>
1218
+ )}
1219
+ </Table.Body>
1220
+ </Table.Root>
1221
+
1222
+ <QuoteParameters />
1223
+
1224
+ <TransactionDetailRow />
1225
+
1226
+ {(status === "completed" || status === "failed") && (
1227
+ <Button
1228
+ onClick={() => setStep(WithdrawalStep.CheckSessionKey)}
1229
+ visual="solid"
1230
+ >
1231
+ {status === "completed" ? "New Deposit" : "Retry Deposit"}
1232
+ </Button>
1233
+ )}
1234
+ </BodyWrapper>
1235
+ );
1236
+ };
1237
+
1238
+ const ExchangeFlow = () => {
1239
+ const { handleClose, setFlow, setStep } = useContext(CheckoutContext);
1240
+ const [currentStep, setCurrentStep] = useState(
1241
+ WithdrawalStep.CheckSessionKey,
1242
+ );
1243
+ const [selectedToken, setSelectedToken] = useState<MatchedToken | null>(
1244
+ null,
1245
+ );
1246
+ const [userOp, setUserOp] = useState<any | null>(null);
1247
+ const { chainIdOut } = useAppStore();
1248
+ const setIsCheckout = useAppStore((state) => state.setIsCheckout);
1249
+ const setChainIdIn = useAppStore((state) => state.setChainIdIn);
1250
+ const walletChainId = useChainId();
1251
+ const { switchChain } = useSwitchChain();
1252
+
1253
+ const wrongChain = walletChainId !== chainIdOut;
1254
+
1255
+ const handleTokenSelect = (token: MatchedToken) => {
1256
+ setSelectedToken(token);
1257
+ console.log("Selected token:", token);
1258
+ };
1259
+
1260
+ useEffect(() => {
1261
+ setChainIdIn(chainIdOut);
1262
+ setIsCheckout(true);
1263
+ }, [chainIdOut]);
1264
+
1265
+ const currentStepComponent = (() => {
1266
+ switch (currentStep) {
1267
+ case WithdrawalStep.CheckSessionKey:
1268
+ return <CheckSessionKeyStep setStep={setCurrentStep} />;
1269
+ case WithdrawalStep.GetHoldings:
1270
+ return (
1271
+ <ChooseAssetStep
1272
+ setStep={setCurrentStep}
1273
+ onTokenSelect={handleTokenSelect}
1274
+ />
1275
+ );
1276
+ case WithdrawalStep.SelectAmount:
1277
+ return (
1278
+ <ChooseAmountStep
1279
+ setStep={setCurrentStep}
1280
+ selectedToken={selectedToken}
1281
+ />
1282
+ );
1283
+ case WithdrawalStep.GetUserOpSignature:
1284
+ return (
1285
+ <SignUserOpStep
1286
+ setStep={setCurrentStep}
1287
+ setUserOp={setUserOp}
1288
+ />
1289
+ );
1290
+ case WithdrawalStep.InitiateWithdrawal:
1291
+ return (
1292
+ <InitiateWithdrawalStep
1293
+ selectedToken={selectedToken}
1294
+ userOp={userOp}
1295
+ setStep={setCurrentStep}
1296
+ />
1297
+ );
1298
+ case WithdrawalStep.TrackUserOp:
1299
+ return (
1300
+ <TrackUserOpStep
1301
+ selectedToken={selectedToken}
1302
+ userOp={userOp}
1303
+ setStep={setCurrentStep}
1304
+ />
1305
+ );
1306
+ default:
1307
+ return null;
1308
+ }
1309
+ })();
1310
+
1311
+ return (
1312
+ <>
1313
+ <Modal.Header>
1314
+ <HeaderWrapper>
1315
+ <IconButton
1316
+ minWidth={"16px"}
1317
+ minHeight={"16px"}
1318
+ maxWidth={"16px"}
1319
+ onClick={() => {
1320
+ const index =
1321
+ withdrawalSteps.findIndex(
1322
+ (step) => step === currentStep,
1323
+ ) - 1;
1324
+ if (index > 0) {
1325
+ setCurrentStep(withdrawalSteps[index]);
1326
+ } else {
1327
+ setFlow("");
1328
+ setStep("");
1329
+ }
1330
+ }}
1331
+ >
1332
+ <Icon
1333
+ as={ChevronLeft}
1334
+ color="gray"
1335
+ width={"16px"}
1336
+ height={"16px"}
1337
+ />
1338
+ </IconButton>
1339
+
1340
+ <Box
1341
+ display="flex"
1342
+ flexDirection="column"
1343
+ gap={"4px"}
1344
+ alignItems={"center"}
1345
+ width="100%"
1346
+ >
1347
+ <HeaderTitle>Deposit from Binance</HeaderTitle>
1348
+ </Box>
1349
+
1350
+ {handleClose && (
1351
+ <IconButton
1352
+ onClick={handleClose}
1353
+ minWidth={"16px"}
1354
+ minHeight={"16px"}
1355
+ maxWidth={"16px"}
1356
+ marginLeft={"auto"}
1357
+ >
1358
+ <Icon
1359
+ as={X}
1360
+ color="gray"
1361
+ width={"16px"}
1362
+ height={"16px"}
1363
+ />
1364
+ </IconButton>
1365
+ )}
1366
+ </HeaderWrapper>
1367
+ </Modal.Header>
1368
+ <Modal.Body>
1369
+ {wrongChain ? (
1370
+ <BodyWrapper>
1371
+ <Box
1372
+ display="flex"
1373
+ flexDirection="column"
1374
+ alignItems="center"
1375
+ gap="16px"
1376
+ textAlign="center"
1377
+ >
1378
+ <Text fontSize="16px" fontWeight="600">
1379
+ Wrong Network
1380
+ </Text>
1381
+ <Text fontSize="14px" color="fg.muted">
1382
+ Please switch to {getChainName(chainIdOut)} to
1383
+ continue with your Binance withdrawal.
1384
+ </Text>
1385
+ <Button
1386
+ onClick={() => {
1387
+ switchChain({ chainId: chainIdOut });
1388
+ }}
1389
+ >
1390
+ Switch to {getChainName(chainIdOut)}
1391
+ </Button>
1392
+ </Box>
1393
+ </BodyWrapper>
1394
+ ) : (
1395
+ currentStepComponent
1396
+ )}
1397
+ </Modal.Body>
1398
+ </>
1399
+ );
1400
+ };
1401
+
1402
+ export default ExchangeFlow;