@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.
- package/dist/checkout-widget.es.js +52889 -0
- package/dist/checkout-widget.es.js.map +1 -0
- package/dist/checkout-widget.umd.js +203 -0
- package/dist/checkout-widget.umd.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/enso-api.yaml +1982 -0
- package/orval.config.ts +25 -0
- package/package.json +79 -0
- package/src/assets/BinanceBadge.svg +4 -0
- package/src/assets/CoinbaseIcon.svg +4 -0
- package/src/assets/USD Coin (USDC).svg +5 -0
- package/src/assets/avecIcon.svg +5 -0
- package/src/assets/base.webp +0 -0
- package/src/assets/depositIcon.svg +6 -0
- package/src/assets/eth.webp +0 -0
- package/src/assets/ethMainnetIcon.svg +10 -0
- package/src/assets/fail.svg +5 -0
- package/src/assets/kraken.png +0 -0
- package/src/assets/logo.svg +10 -0
- package/src/assets/mastercard.png +0 -0
- package/src/assets/metamask.png +0 -0
- package/src/assets/rabby.png +0 -0
- package/src/assets/success.svg +4 -0
- package/src/assets/usdc.webp +0 -0
- package/src/assets/usdt.webp +0 -0
- package/src/assets/visa.png +0 -0
- package/src/assets/visa.webp +0 -0
- package/src/components/BridgeFee.tsx +58 -0
- package/src/components/ChakraProvider.tsx +372 -0
- package/src/components/Checkout.tsx +127 -0
- package/src/components/CheckoutModal.tsx +22 -0
- package/src/components/CircleTimer.tsx +66 -0
- package/src/components/CurrencySwapDisplay.tsx +153 -0
- package/src/components/DepositProcessing.tsx +116 -0
- package/src/components/ExchangeConfirmSecurity.tsx +110 -0
- package/src/components/QuoteParameters.tsx +341 -0
- package/src/components/TransactionDetailRow.tsx +124 -0
- package/src/components/cards/AssetCard.tsx +167 -0
- package/src/components/cards/ExchangeCard.tsx +53 -0
- package/src/components/cards/OptionCard.tsx +59 -0
- package/src/components/cards/WalletCard.tsx +99 -0
- package/src/components/cards/index.ts +6 -0
- package/src/components/modal.tsx +83 -0
- package/src/components/steps/ExchangeFlow.tsx +1402 -0
- package/src/components/steps/InitialStep.tsx +169 -0
- package/src/components/steps/QuoteStep.tsx +121 -0
- package/src/components/steps/WalletAmountStep.tsx +258 -0
- package/src/components/steps/WalletConfirmStep.tsx +404 -0
- package/src/components/steps/WalletTokenStep.tsx +128 -0
- package/src/components/ui/index.tsx +394 -0
- package/src/components/ui/styled.tsx +85 -0
- package/src/components/ui/toaster.tsx +43 -0
- package/src/components/ui/tooltip.tsx +46 -0
- package/src/enso-api/api.ts +173 -0
- package/src/enso-api/custom-instance.ts +35 -0
- package/src/enso-api/index.ts +5119 -0
- package/src/enso-api/model/action.ts +17 -0
- package/src/enso-api/model/actionAction.ts +52 -0
- package/src/enso-api/model/actionInputs.ts +12 -0
- package/src/enso-api/model/actionToBundle.ts +19 -0
- package/src/enso-api/model/actionToBundleAction.ts +53 -0
- package/src/enso-api/model/actionToBundleArgs.ts +12 -0
- package/src/enso-api/model/bundleControllerBundleShortcutTransactionParams.ts +53 -0
- package/src/enso-api/model/bundleControllerBundleShortcutTransactionRoutingStrategy.ts +23 -0
- package/src/enso-api/model/bundleShortcutTransaction.ts +35 -0
- package/src/enso-api/model/bundleShortcutTransactionAmountsOut.ts +15 -0
- package/src/enso-api/model/bundleShortcutTransactionFeeAmount.ts +12 -0
- package/src/enso-api/model/connectedNetwork.ts +16 -0
- package/src/enso-api/model/hop.ts +24 -0
- package/src/enso-api/model/hopArgs.ts +12 -0
- package/src/enso-api/model/index.ts +70 -0
- package/src/enso-api/model/iporControllerIporShortcutTransactionParams.ts +21 -0
- package/src/enso-api/model/iporShortcutInput.ts +33 -0
- package/src/enso-api/model/iporShortcutTransaction.ts +22 -0
- package/src/enso-api/model/lZDestinationTokenData.ts +19 -0
- package/src/enso-api/model/lZPoolLookupResponse.ts +26 -0
- package/src/enso-api/model/layerZeroControllerGetPoolAddressParams.ts +29 -0
- package/src/enso-api/model/network.ts +15 -0
- package/src/enso-api/model/networksControllerNetworksParams.ts +21 -0
- package/src/enso-api/model/nonTokenizedControllerTokens200.ts +15 -0
- package/src/enso-api/model/nonTokenizedControllerTokens200AllOf.ts +16 -0
- package/src/enso-api/model/nonTokenizedControllerTokensParams.ts +41 -0
- package/src/enso-api/model/nonTokenizedModel.ts +27 -0
- package/src/enso-api/model/nontokenizedControllerRouteNontokenizedShorcutTransactionParams.ts +64 -0
- package/src/enso-api/model/nontokenizedControllerRouteNontokenizedShorcutTransactionRoutingStrategy.ts +22 -0
- package/src/enso-api/model/paginatedResult.ts +16 -0
- package/src/enso-api/model/paginationMeta.ts +27 -0
- package/src/enso-api/model/positionModel.ts +77 -0
- package/src/enso-api/model/price.ts +20 -0
- package/src/enso-api/model/pricesControllerGetPricesParams.ts +17 -0
- package/src/enso-api/model/project.ts +15 -0
- package/src/enso-api/model/protocol.ts +15 -0
- package/src/enso-api/model/protocolModel.ts +26 -0
- package/src/enso-api/model/protocolsControllerFindAllParams.ts +21 -0
- package/src/enso-api/model/routeShortcutTransaction.ts +33 -0
- package/src/enso-api/model/routeShortcutVariableInputs.ts +68 -0
- package/src/enso-api/model/routeShortcutVariableInputsRoutingStrategy.ts +27 -0
- package/src/enso-api/model/routeShortcutVariableInputsVariableEstimates.ts +14 -0
- package/src/enso-api/model/routerControllerRouteShortcutTransactionParams.ts +91 -0
- package/src/enso-api/model/routerControllerRouteShortcutTransactionRoutingStrategy.ts +23 -0
- package/src/enso-api/model/standard.ts +18 -0
- package/src/enso-api/model/standardAction.ts +20 -0
- package/src/enso-api/model/standardActionAction.ts +53 -0
- package/src/enso-api/model/tokenModel.ts +36 -0
- package/src/enso-api/model/tokensControllerTokens200.ts +15 -0
- package/src/enso-api/model/tokensControllerTokens200AllOf.ts +16 -0
- package/src/enso-api/model/tokensControllerTokensParams.ts +91 -0
- package/src/enso-api/model/tokensControllerTokensType.ts +19 -0
- package/src/enso-api/model/transaction.ts +17 -0
- package/src/enso-api/model/userOperation.ts +28 -0
- package/src/enso-api/model/walletApproveTransaction.ts +24 -0
- package/src/enso-api/model/walletApproveTransactionTx.ts +15 -0
- package/src/enso-api/model/walletBalance.ts +29 -0
- package/src/enso-api/model/walletControllerCreateApproveTransactionParams.ts +35 -0
- package/src/enso-api/model/walletControllerCreateApproveTransactionRoutingStrategy.ts +23 -0
- package/src/enso-api/model/walletControllerWalletBalancesParams.ts +25 -0
- package/src/index.ts +17 -0
- package/src/store.ts +68 -0
- package/src/types/assets.d.ts +29 -0
- package/src/types/index.ts +21 -0
- package/src/util/common.tsx +324 -0
- package/src/util/constants.tsx +213 -0
- package/src/util/enso-hooks.tsx +203 -0
- package/src/util/index.tsx +68 -0
- package/src/util/tx-tracker.tsx +301 -0
- package/src/util/wallet.tsx +258 -0
- package/tsconfig.json +13 -0
- 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;
|