@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.
- package/dist/checkout-widget.es.js +24133 -25422
- package/dist/checkout-widget.umd.js +59 -65
- package/dist/index.d.ts +1 -3
- package/package.json +2 -2
- package/src/assets/usdc.webp +0 -0
- package/src/assets/usdt.webp +0 -0
- package/src/components/AmountInput.tsx +25 -41
- package/src/components/ChakraProvider.tsx +13 -36
- package/src/components/Checkout.tsx +0 -3
- package/src/components/CurrencySwapDisplay.tsx +22 -59
- package/src/components/DepositProcessing.tsx +1 -1
- package/src/components/ExchangeConfirmSecurity.tsx +1 -1
- package/src/components/QuoteParameters.tsx +1 -1
- package/src/components/TransactionDetailRow.tsx +2 -2
- package/src/components/cards/ExchangeCard.tsx +1 -1
- package/src/components/cards/OptionCard.tsx +1 -2
- package/src/components/cards/WalletCard.tsx +1 -1
- package/src/components/modal.tsx +3 -3
- package/src/components/steps/ExchangeFlow.tsx +1404 -231
- package/src/components/steps/FlowSelector.tsx +60 -117
- package/src/components/steps/WalletFlow/WalletAmountStep.tsx +2 -2
- package/src/components/steps/WalletFlow/WalletConfirmStep.tsx +51 -92
- package/src/components/steps/WalletFlow/WalletFlow.tsx +16 -17
- package/src/components/steps/WalletFlow/WalletQuoteStep.tsx +2 -2
- package/src/components/steps/WalletFlow/WalletTokenStep.tsx +4 -6
- package/src/components/ui/index.tsx +6 -23
- package/src/components/ui/toaster.tsx +1 -2
- package/src/types/index.ts +0 -97
- package/src/util/constants.tsx +0 -27
- package/src/util/enso-hooks.tsx +61 -75
- package/dist/checkout-widget.es.js.map +0 -1
- package/dist/checkout-widget.umd.js.map +0 -1
- package/src/assets/providers/alchemypay.svg +0 -21
- package/src/assets/providers/banxa.svg +0 -21
- package/src/assets/providers/binanceconnect.svg +0 -14
- package/src/assets/providers/kryptonim.svg +0 -6
- package/src/assets/providers/mercuryo.svg +0 -21
- package/src/assets/providers/moonpay.svg +0 -14
- package/src/assets/providers/stripe.svg +0 -16
- package/src/assets/providers/swapped.svg +0 -1
- package/src/assets/providers/topper.svg +0 -14
- package/src/assets/providers/transak.svg +0 -21
- package/src/assets/providers/unlimit.svg +0 -21
- package/src/components/steps/CardBuyFlow/CardBuyFlow.tsx +0 -412
- package/src/components/steps/CardBuyFlow/ChooseAmountStep.tsx +0 -352
- package/src/components/steps/CardBuyFlow/OpenWidgetStep.tsx +0 -193
- package/src/components/steps/SmartAccountFlow.tsx +0 -372
- package/src/components/steps/shared/ChooseAmountStep.tsx +0 -325
- package/src/components/steps/shared/SignUserOpStep.tsx +0 -117
- package/src/components/steps/shared/TrackUserOpStep.tsx +0 -625
- package/src/components/steps/shared/exchangeIntegration.ts +0 -19
- package/src/components/steps/shared/types.ts +0 -22
- package/src/components/ui/transitions.tsx +0 -16
- package/src/enso-api/model/bridgeTransactionResponse.ts +0 -37
- package/src/enso-api/model/bridgeTransactionResponseStatus.ts +0 -25
- package/src/enso-api/model/ensoEvent.ts +0 -30
- package/src/enso-api/model/ensoMetadata.ts +0 -23
- package/src/enso-api/model/layerZeroControllerCheckBridgeTransactionParams.ts +0 -21
- package/src/enso-api/model/layerZeroMessageStatus.ts +0 -39
- package/src/enso-api/model/refundDetails.ts +0 -21
- 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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
`${
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
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: {
|
|
684
|
-
|
|
1180
|
+
transferOptions: {
|
|
1181
|
+
toAddresses,
|
|
1182
|
+
},
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
console.log("link request body", meshData);
|
|
685
1186
|
|
|
686
1187
|
const response = await fetch(
|
|
687
|
-
`${
|
|
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: (
|
|
1956
|
+
setFlow: (string) => void;
|
|
1957
|
+
initialStep?: WithdrawalStep;
|
|
824
1958
|
}) => {
|
|
825
1959
|
const { handleClose, enforceFlow } = useContext(CheckoutContext);
|
|
826
|
-
const [currentStep, setCurrentStep] = useState
|
|
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 () =>
|
|
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
|
-
<
|
|
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
|
-
<
|
|
865
|
-
nextStep={
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
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="
|
|
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
|
-
|
|
955
|
-
|
|
956
|
-
|
|
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
|
);
|