@ensofinance/checkout-widget 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/checkout-widget.es.js +25627 -24338
  2. package/dist/checkout-widget.es.js.map +1 -1
  3. package/dist/checkout-widget.umd.js +64 -59
  4. package/dist/checkout-widget.umd.js.map +1 -1
  5. package/dist/index.d.ts +5 -1
  6. package/package.json +1 -1
  7. package/src/assets/providers/alchemypay.svg +21 -0
  8. package/src/assets/providers/banxa.svg +21 -0
  9. package/src/assets/providers/binanceconnect.svg +14 -0
  10. package/src/assets/providers/kryptonim.svg +6 -0
  11. package/src/assets/providers/mercuryo.svg +21 -0
  12. package/src/assets/providers/moonpay.svg +14 -0
  13. package/src/assets/providers/stripe.svg +16 -0
  14. package/src/assets/providers/swapped.svg +1 -0
  15. package/src/assets/providers/topper.svg +14 -0
  16. package/src/assets/providers/transak.svg +21 -0
  17. package/src/assets/providers/unlimit.svg +21 -0
  18. package/src/components/AmountInput.tsx +41 -25
  19. package/src/components/ChakraProvider.tsx +36 -13
  20. package/src/components/Checkout.tsx +7 -1
  21. package/src/components/CurrencySwapDisplay.tsx +59 -22
  22. package/src/components/DepositProcessing.tsx +1 -1
  23. package/src/components/ExchangeConfirmSecurity.tsx +1 -1
  24. package/src/components/QuoteParameters.tsx +1 -1
  25. package/src/components/TransactionDetailRow.tsx +2 -2
  26. package/src/components/cards/ExchangeCard.tsx +1 -1
  27. package/src/components/cards/OptionCard.tsx +2 -1
  28. package/src/components/cards/WalletCard.tsx +1 -1
  29. package/src/components/modal.tsx +3 -3
  30. package/src/components/steps/CardBuyFlow/CardBuyFlow.tsx +412 -0
  31. package/src/components/steps/CardBuyFlow/ChooseAmountStep.tsx +352 -0
  32. package/src/components/steps/CardBuyFlow/OpenWidgetStep.tsx +193 -0
  33. package/src/components/steps/ExchangeFlow.tsx +231 -1404
  34. package/src/components/steps/FlowSelector.tsx +117 -60
  35. package/src/components/steps/SmartAccountFlow.tsx +372 -0
  36. package/src/components/steps/WalletFlow/WalletAmountStep.tsx +2 -2
  37. package/src/components/steps/WalletFlow/WalletConfirmStep.tsx +92 -51
  38. package/src/components/steps/WalletFlow/WalletFlow.tsx +17 -16
  39. package/src/components/steps/WalletFlow/WalletQuoteStep.tsx +2 -2
  40. package/src/components/steps/WalletFlow/WalletTokenStep.tsx +6 -4
  41. package/src/components/steps/shared/ChooseAmountStep.tsx +325 -0
  42. package/src/components/steps/shared/SignUserOpStep.tsx +117 -0
  43. package/src/components/steps/shared/TrackUserOpStep.tsx +625 -0
  44. package/src/components/steps/shared/exchangeIntegration.ts +19 -0
  45. package/src/components/steps/shared/types.ts +22 -0
  46. package/src/components/ui/index.tsx +23 -6
  47. package/src/components/ui/toaster.tsx +2 -1
  48. package/src/components/ui/transitions.tsx +16 -0
  49. package/src/enso-api/model/bridgeTransactionResponse.ts +37 -0
  50. package/src/enso-api/model/bridgeTransactionResponseStatus.ts +25 -0
  51. package/src/enso-api/model/ensoEvent.ts +30 -0
  52. package/src/enso-api/model/ensoMetadata.ts +23 -0
  53. package/src/enso-api/model/layerZeroControllerCheckBridgeTransactionParams.ts +21 -0
  54. package/src/enso-api/model/layerZeroMessageStatus.ts +39 -0
  55. package/src/enso-api/model/refundDetails.ts +21 -0
  56. package/src/types/index.ts +99 -0
  57. package/src/util/constants.tsx +27 -0
  58. package/src/util/enso-hooks.tsx +75 -61
  59. package/src/util/meld-hooks.tsx +533 -0
  60. package/src/assets/usdc.webp +0 -0
  61. package/src/assets/usdt.webp +0 -0
@@ -0,0 +1,325 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { Box } from "@chakra-ui/react";
3
+
4
+ import { BodyWrapper, HeaderTitle, HeaderDescription } from "../../ui/styled";
5
+ import { Button } from "../../ui";
6
+ import { AmountInput, AmountInputValue } from "../../AmountInput";
7
+ import { useAppStore } from "@/store";
8
+ import {
9
+ denormalizeValue,
10
+ compareCaseInsensitive,
11
+ formatNumber,
12
+ formatUSD,
13
+ normalizeValue,
14
+ } from "@/util";
15
+ import {
16
+ precisionizeNumber,
17
+ getPositiveDecimalValue,
18
+ } from "@/util/common";
19
+ import {
20
+ EXCHANGE_MAX_LIMIT_GAP_USD,
21
+ EXCHANGE_MIN_LIMIT,
22
+ } from "@/util/constants";
23
+ import { useAppDetails } from "@/util/enso-hooks";
24
+ import type { MatchedToken } from "./types";
25
+
26
+ const ChooseAmountStep = ({
27
+ setStep,
28
+ nextStep,
29
+ selectedToken,
30
+ prefillAmountToken,
31
+ mode,
32
+ }: {
33
+ setStep: (step: number) => void;
34
+ nextStep: number;
35
+ selectedToken: MatchedToken | null;
36
+ prefillAmountToken?: string | null;
37
+ mode: "cex" | "smart-account";
38
+ }) => {
39
+ const [amountInput, setAmountInput] = useState<AmountInputValue>({
40
+ tokenAmount: "",
41
+ usdAmount: "",
42
+ mode: "usd",
43
+ });
44
+ const initializedTokenKeyRef = useRef<string | null>(null);
45
+ const setAmountIn = useAppStore((s) => s.setAmountIn);
46
+ const amountInStore = useAppStore((s) => s.amountIn);
47
+ const chainIdInStore = useAppStore((s) => s.chainIdIn);
48
+ const tokenInStore = useAppStore((s) => s.tokenIn);
49
+ const { tokenInData } = useAppDetails();
50
+ const isStable = selectedToken?.symbol.toLowerCase().includes("USD");
51
+ const roundingPrecision = isStable ? 2 : 6;
52
+ const amount = amountInput.tokenAmount;
53
+ const usdValue = amountInput.usdAmount;
54
+
55
+ const isCex = mode === "cex";
56
+
57
+ const maxUsdAmount = selectedToken
58
+ ? isCex
59
+ ? (selectedToken.marketValue - EXCHANGE_MAX_LIMIT_GAP_USD).toFixed(2)
60
+ : selectedToken.marketValue.toFixed(2)
61
+ : 0;
62
+
63
+ const tokenPriceUsd = useMemo(() => {
64
+ if (!selectedToken || !selectedToken.balance) return undefined;
65
+ return selectedToken.marketValue / selectedToken.balance;
66
+ }, [selectedToken]);
67
+
68
+ const getPercentAmounts = useCallback(
69
+ (percent: number) => {
70
+ if (!selectedToken) return;
71
+
72
+ const targetUsdAmount = (selectedToken.marketValue * percent) / 100;
73
+
74
+ let finalUsdAmount: number;
75
+ let finalTokenAmount: number;
76
+
77
+ if (isCex) {
78
+ const minValueForToken =
79
+ EXCHANGE_MIN_LIMIT[
80
+ selectedToken.symbol as keyof typeof EXCHANGE_MIN_LIMIT
81
+ ] || 0;
82
+
83
+ finalUsdAmount = Math.max(
84
+ minValueForToken,
85
+ Math.min(targetUsdAmount, +maxUsdAmount),
86
+ );
87
+
88
+ const tokenPrice =
89
+ selectedToken.marketValue / selectedToken.balance;
90
+ finalTokenAmount = Math.min(
91
+ finalUsdAmount / tokenPrice,
92
+ selectedToken.balance,
93
+ );
94
+ } else {
95
+ finalUsdAmount = targetUsdAmount;
96
+ finalTokenAmount = (selectedToken.balance * percent) / 100;
97
+ }
98
+
99
+ return {
100
+ tokenAmount: precisionizeNumber(
101
+ finalTokenAmount,
102
+ roundingPrecision,
103
+ ),
104
+ usdAmount: finalUsdAmount.toFixed(2),
105
+ };
106
+ },
107
+ [selectedToken, isCex, maxUsdAmount, roundingPrecision],
108
+ );
109
+
110
+ // Set initial value on token change.
111
+ useEffect(() => {
112
+ if (!selectedToken) return;
113
+
114
+ const tokenKey = [
115
+ selectedToken.chainId,
116
+ selectedToken.symbol,
117
+ selectedToken.tokenAddress ?? "",
118
+ ].join(":");
119
+ if (initializedTokenKeyRef.current === tokenKey) return;
120
+
121
+ const storeMatchesSelectedToken =
122
+ !!selectedToken.tokenAddress &&
123
+ !!chainIdInStore &&
124
+ chainIdInStore === selectedToken.chainId &&
125
+ compareCaseInsensitive(tokenInStore, selectedToken.tokenAddress);
126
+
127
+ let tokenAmountToUse: string | null = null;
128
+
129
+ if (
130
+ storeMatchesSelectedToken &&
131
+ tokenInData?.decimals &&
132
+ amountInStore &&
133
+ amountInStore !== "0"
134
+ ) {
135
+ const normalizedStoreAmount = normalizeValue(
136
+ amountInStore,
137
+ tokenInData.decimals,
138
+ );
139
+ const normalizedNumber = Number(normalizedStoreAmount);
140
+ if (Number.isFinite(normalizedNumber) && normalizedNumber > 0) {
141
+ tokenAmountToUse = precisionizeNumber(
142
+ normalizedNumber,
143
+ roundingPrecision,
144
+ );
145
+ }
146
+ }
147
+
148
+ if (!tokenAmountToUse && prefillAmountToken) {
149
+ const prefillNumber = Number(prefillAmountToken);
150
+ if (Number.isFinite(prefillNumber) && prefillNumber > 0) {
151
+ tokenAmountToUse = precisionizeNumber(
152
+ prefillNumber,
153
+ roundingPrecision,
154
+ );
155
+ }
156
+ }
157
+
158
+ if (tokenAmountToUse) {
159
+ const usdAmountToUse =
160
+ tokenPriceUsd && tokenPriceUsd > 0
161
+ ? (Number(tokenAmountToUse) * tokenPriceUsd).toFixed(2)
162
+ : "";
163
+
164
+ setAmountInput((prev) => ({
165
+ ...prev,
166
+ tokenAmount: tokenAmountToUse as string,
167
+ usdAmount: usdAmountToUse,
168
+ }));
169
+ initializedTokenKeyRef.current = tokenKey;
170
+ return;
171
+ }
172
+
173
+ const percentAmounts = getPercentAmounts(100);
174
+ if (!percentAmounts) return;
175
+
176
+ setAmountInput((prev) => ({
177
+ ...prev,
178
+ ...percentAmounts,
179
+ }));
180
+ initializedTokenKeyRef.current = tokenKey;
181
+ }, [
182
+ amountInStore,
183
+ chainIdInStore,
184
+ getPercentAmounts,
185
+ prefillAmountToken,
186
+ roundingPrecision,
187
+ selectedToken,
188
+ tokenInData?.decimals,
189
+ tokenInStore,
190
+ tokenPriceUsd,
191
+ ]);
192
+
193
+ useEffect(() => {
194
+ if (!tokenInData?.decimals) return;
195
+
196
+ const normalizedAmount = amount.endsWith(".")
197
+ ? amount.slice(0, -1)
198
+ : amount;
199
+
200
+ if (!normalizedAmount || normalizedAmount === ".") {
201
+ setAmountIn("0");
202
+ return;
203
+ }
204
+
205
+ try {
206
+ setAmountIn(
207
+ denormalizeValue(normalizedAmount, tokenInData.decimals),
208
+ );
209
+ } catch (error) {
210
+ setAmountIn("0");
211
+ }
212
+ }, [amount, tokenInData?.decimals, setAmountIn]);
213
+
214
+ const numericAmount = getPositiveDecimalValue(amount);
215
+ const hasPositiveAmount = numericAmount !== null;
216
+ const hasUsdValue = !!usdValue && usdValue !== ".";
217
+ const notEnoughBalance = selectedToken
218
+ ? hasPositiveAmount &&
219
+ numericAmount !== null &&
220
+ numericAmount > selectedToken.balance
221
+ : true;
222
+
223
+ // Limits validation logic - only for CEX withdrawals
224
+ const currentUsdValue = hasUsdValue
225
+ ? (getPositiveDecimalValue(usdValue) ?? 0)
226
+ : 0;
227
+ const minValueForToken =
228
+ isCex && selectedToken
229
+ ? EXCHANGE_MIN_LIMIT[
230
+ selectedToken.symbol as keyof typeof EXCHANGE_MIN_LIMIT
231
+ ]
232
+ : 0;
233
+
234
+ const isBelowMinAmount =
235
+ isCex &&
236
+ selectedToken &&
237
+ hasPositiveAmount &&
238
+ currentUsdValue > 0 &&
239
+ minValueForToken &&
240
+ numericAmount !== null &&
241
+ numericAmount < minValueForToken;
242
+ const isAboveMaxAmount =
243
+ isCex &&
244
+ selectedToken &&
245
+ hasPositiveAmount &&
246
+ currentUsdValue > 0 &&
247
+ currentUsdValue > +maxUsdAmount;
248
+
249
+ const isAmountInvalid =
250
+ !hasPositiveAmount ||
251
+ isBelowMinAmount ||
252
+ isAboveMaxAmount ||
253
+ notEnoughBalance;
254
+
255
+ if (!selectedToken) {
256
+ return (
257
+ <BodyWrapper>
258
+ <Box textAlign="center" color="fg.subtle" py={8}>
259
+ No token selected
260
+ </Box>
261
+ </BodyWrapper>
262
+ );
263
+ }
264
+
265
+ return (
266
+ <BodyWrapper>
267
+ <Box mb={4} width="100%" textAlign="left">
268
+ <HeaderTitle>Enter Amount</HeaderTitle>
269
+ <HeaderDescription>
270
+ Available: {formatNumber(selectedToken.balance)}{" "}
271
+ {selectedToken.symbol} (
272
+ {formatUSD(selectedToken.marketValue)})
273
+ </HeaderDescription>
274
+ </Box>
275
+
276
+ <Box
277
+ display={"flex"}
278
+ flexDirection={"column"}
279
+ gap={"8px"}
280
+ width="100%"
281
+ >
282
+ <AmountInput
283
+ value={amountInput}
284
+ onChange={setAmountInput}
285
+ tokenSymbol={selectedToken.symbol}
286
+ tokenPriceUsd={tokenPriceUsd}
287
+ roundingPrecision={roundingPrecision}
288
+ onPercentSelect={getPercentAmounts}
289
+ />
290
+ </Box>
291
+
292
+ {
293
+ <Box
294
+ textAlign="center"
295
+ color="fg.subtle"
296
+ fontSize="xs"
297
+ h={3}
298
+ m={-1}
299
+ visibility={isAmountInvalid ? "visible" : "hidden"}
300
+ >
301
+ {!hasPositiveAmount
302
+ ? "Please enter an amount"
303
+ : isBelowMinAmount
304
+ ? `Minimum amount is ${formatNumber(minValueForToken)} ${selectedToken.symbol}`
305
+ : isAboveMaxAmount
306
+ ? `Maximum amount is ${formatUSD(maxUsdAmount)} (balance - $${EXCHANGE_MAX_LIMIT_GAP_USD})`
307
+ : notEnoughBalance
308
+ ? "Amount exceeds available balance"
309
+ : ""}
310
+ </Box>
311
+ }
312
+
313
+ <Button
314
+ onClick={() =>
315
+ isAmountInvalid ? undefined : setStep(nextStep)
316
+ }
317
+ disabled={isAmountInvalid}
318
+ >
319
+ Continue
320
+ </Button>
321
+ </BodyWrapper>
322
+ );
323
+ };
324
+
325
+ export default ChooseAmountStep;
@@ -0,0 +1,117 @@
1
+ import { useState } from "react";
2
+ import { Flex, Skeleton } from "@chakra-ui/react";
3
+ import { useAccount, useSignMessage } from "wagmi";
4
+ import { getUserOperationHash } from "viem/account-abstraction";
5
+
6
+ import { BodyWrapper } from "../../ui/styled";
7
+ import { Button, Input } from "../../ui";
8
+ import QuoteParameters from "../../QuoteParameters";
9
+ import { TransactionDetailRow } from "../../TransactionDetailRow";
10
+ import { useAppDetails, useRouteData } from "@/util/enso-hooks";
11
+ import { ENTRY_POINT_ADDRESS } from "@/util/constants";
12
+
13
+ const SignUserOpStep = ({
14
+ setStep,
15
+ setUserOp,
16
+ nextStep,
17
+ }: {
18
+ nextStep: number;
19
+ setStep: (step: number) => void;
20
+ setUserOp: (userOp: any) => void;
21
+ }) => {
22
+ const { chainIdIn } = useAppDetails();
23
+ const { routeLoading, usdAmountIn, routeData } = useRouteData();
24
+ const { signMessageAsync } = useSignMessage();
25
+ const { address } = useAccount();
26
+ const [isSigning, setIsSigning] = useState(false);
27
+
28
+ const handleSignUserOp = async () => {
29
+ if (!routeData || (routeData as any)?.error) {
30
+ console.error("No valid router data available");
31
+ return;
32
+ }
33
+
34
+ try {
35
+ setIsSigning(true);
36
+
37
+ const userOperation = routeData?.userOp;
38
+ if (!userOperation) {
39
+ console.error("No userOperation found in routeData");
40
+ return;
41
+ }
42
+
43
+ console.log("Signing userOperation:", userOperation);
44
+
45
+ const userOpHash = getUserOperationHash({
46
+ // @ts-ignore
47
+ userOperation,
48
+ entryPointAddress: ENTRY_POINT_ADDRESS,
49
+ entryPointVersion: "0.7",
50
+ chainId: chainIdIn,
51
+ });
52
+
53
+ const signature = await signMessageAsync({
54
+ account: address as `0x${string}`,
55
+ message: { raw: userOpHash as `0x${string}` },
56
+ });
57
+
58
+ const signedUserOp = {
59
+ ...userOperation,
60
+ signature,
61
+ };
62
+
63
+ console.log("signedUserOp", JSON.stringify(signedUserOp));
64
+
65
+ setUserOp(signedUserOp);
66
+ setStep(nextStep);
67
+ } catch (error) {
68
+ console.error("Failed to sign userOperation:", error);
69
+ } finally {
70
+ setIsSigning(false);
71
+ }
72
+ };
73
+
74
+ return (
75
+ <BodyWrapper>
76
+ <Flex
77
+ flexDirection={"column"}
78
+ gap={"16px"}
79
+ alignItems={"center"}
80
+ width={"100%"}
81
+ >
82
+ <Skeleton
83
+ loading={routeLoading}
84
+ width={routeLoading ? "156px" : "auto"}
85
+ >
86
+ <Input
87
+ readOnly
88
+ marginY={"8px"}
89
+ variant={"text"}
90
+ placeholder={"$10.00"}
91
+ value={usdAmountIn}
92
+ />
93
+ </Skeleton>
94
+
95
+ <QuoteParameters />
96
+ </Flex>
97
+
98
+ <TransactionDetailRow />
99
+
100
+ <Button
101
+ disabled={
102
+ !!(routeData as any)?.message || routeLoading || isSigning
103
+ }
104
+ loading={routeLoading || isSigning}
105
+ onClick={handleSignUserOp}
106
+ >
107
+ {routeLoading
108
+ ? "Loading quote"
109
+ : isSigning
110
+ ? "Signing..."
111
+ : "Sign Transaction"}
112
+ </Button>
113
+ </BodyWrapper>
114
+ );
115
+ };
116
+
117
+ export default SignUserOpStep;