@ensofinance/checkout-widget 0.0.12 → 0.0.13

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.
@@ -0,0 +1,116 @@
1
+ import { useState, useContext } from "react";
2
+ import { Box, Icon } from "@chakra-ui/react";
3
+ import { ChevronLeft, X } from "lucide-react";
4
+ import WalletTokenStep from "./WalletTokenStep";
5
+ import WalletAmountStep from "./WalletAmountStep";
6
+ import WalletQuoteStep from "./WalletQuoteStep";
7
+ import WalletConfirmStep from "./WalletConfirmStep";
8
+ import { CheckoutContext } from "@/components/Checkout";
9
+ import Modal from "@/components/modal";
10
+ import { IconButton } from "@/components/ui";
11
+ import { HeaderWrapper, HeaderTitle } from "@/components/ui/styled";
12
+
13
+ export enum WalletFlowStep {
14
+ SelectToken = "selectToken",
15
+ SelectAmount = "selectAmount",
16
+ Quote = "quote",
17
+ ConfirmTransfer = "confirmTransfer",
18
+ }
19
+
20
+ const walletSteps = [
21
+ WalletFlowStep.SelectToken,
22
+ WalletFlowStep.SelectAmount,
23
+ WalletFlowStep.Quote,
24
+ WalletFlowStep.ConfirmTransfer,
25
+ ];
26
+
27
+ const WalletFlow = ({
28
+ initialStep = WalletFlowStep.SelectToken,
29
+ setFlow,
30
+ }: {
31
+ initialStep?: WalletFlowStep;
32
+ setFlow?: (step: string) => void;
33
+ }) => {
34
+ const [currentStep, setCurrentStep] = useState<WalletFlowStep>(initialStep);
35
+ const { handleClose } = useContext(CheckoutContext);
36
+
37
+ const handleSetStep = (step: WalletFlowStep | string) => {
38
+ setCurrentStep(step as WalletFlowStep);
39
+ };
40
+
41
+ const handleBackClick = () => {
42
+ const index = walletSteps.findIndex((step) => step === currentStep) - 1;
43
+ if (index >= 0) {
44
+ setCurrentStep(walletSteps[index]);
45
+ } else if (setFlow) {
46
+ setFlow("");
47
+ }
48
+ };
49
+
50
+ const currentStepComponent = (() => {
51
+ switch (currentStep) {
52
+ case WalletFlowStep.SelectToken:
53
+ return <WalletTokenStep setStep={handleSetStep} />;
54
+ case WalletFlowStep.SelectAmount:
55
+ return <WalletAmountStep setStep={handleSetStep} />;
56
+ case WalletFlowStep.Quote:
57
+ return <WalletQuoteStep setStep={handleSetStep} />;
58
+ case WalletFlowStep.ConfirmTransfer:
59
+ return <WalletConfirmStep setStep={handleSetStep} />;
60
+ default:
61
+ return <WalletTokenStep setStep={handleSetStep} />;
62
+ }
63
+ })();
64
+
65
+ return (
66
+ <>
67
+ <Modal.Header>
68
+ <HeaderWrapper>
69
+ <IconButton
70
+ minWidth={"16px"}
71
+ minHeight={"16px"}
72
+ maxWidth={"16px"}
73
+ onClick={handleBackClick}
74
+ >
75
+ <Icon
76
+ as={ChevronLeft}
77
+ color="gray"
78
+ width={"16px"}
79
+ height={"16px"}
80
+ />
81
+ </IconButton>
82
+
83
+ <Box
84
+ display="flex"
85
+ flexDirection="column"
86
+ gap={"4px"}
87
+ alignItems={"center"}
88
+ width="100%"
89
+ >
90
+ <HeaderTitle>Wallet Deposit</HeaderTitle>
91
+ </Box>
92
+
93
+ {handleClose && (
94
+ <IconButton
95
+ onClick={handleClose}
96
+ minWidth={"16px"}
97
+ minHeight={"16px"}
98
+ maxWidth={"16px"}
99
+ marginLeft={"auto"}
100
+ >
101
+ <Icon
102
+ as={X}
103
+ color="gray"
104
+ width={"16px"}
105
+ height={"16px"}
106
+ />
107
+ </IconButton>
108
+ )}
109
+ </HeaderWrapper>
110
+ </Modal.Header>
111
+ <Modal.Body>{currentStepComponent}</Modal.Body>
112
+ </>
113
+ );
114
+ };
115
+
116
+ export default WalletFlow;
@@ -1,19 +1,13 @@
1
- import { useContext } from "react";
2
- import { Box, Icon, Skeleton, Flex } from "@chakra-ui/react";
3
- import { ChevronLeft, X } from "lucide-react";
1
+ import { Skeleton, Flex } from "@chakra-ui/react";
4
2
  import { useChainId, useSwitchChain } from "wagmi";
5
- import Modal from "../modal";
6
- import { CheckoutContext } from "../Checkout";
7
- import { Button, IconButton, Input } from "../ui";
8
- import { HeaderWrapper, HeaderTitle, BodyWrapper } from "../ui/styled";
9
- import { TransactionDetailRow } from "../TransactionDetailRow";
3
+ import { Button, Input } from "@/components/ui";
4
+ import { BodyWrapper } from "@/components/ui/styled";
5
+ import { TransactionDetailRow } from "@/components/TransactionDetailRow";
10
6
  import { useAppDetails, useRouteData } from "@/util/enso-hooks";
11
7
  import { useChainName } from "@/util/common";
12
- import QuoteParameters from "../QuoteParameters";
13
-
14
- const QuoteStep = () => {
15
- const { handleClose, setStep } = useContext(CheckoutContext);
8
+ import QuoteParameters from "@/components/QuoteParameters";
16
9
 
10
+ const WalletQuoteStep = ({ setStep }: { setStep: (step: string) => void }) => {
17
11
  const { chainIdIn } = useAppDetails();
18
12
 
19
13
  const { routeLoading, usdAmountIn, routeData } = useRouteData();
@@ -24,48 +18,7 @@ const QuoteStep = () => {
24
18
  const wrongChain = walletChainId !== chainIdIn;
25
19
 
26
20
  return (
27
- <>
28
- <Modal.Header>
29
- <HeaderWrapper>
30
- <IconButton
31
- minWidth={"16px"}
32
- minHeight={"16px"}
33
- maxWidth={"16px"}
34
- onClick={() => {
35
- setStep("selectAmount");
36
- }}
37
- >
38
- <Icon
39
- as={ChevronLeft}
40
- color="gray"
41
- width={"16px"}
42
- height={"16px"}
43
- />
44
- </IconButton>
45
-
46
- <Box display="flex" flexDirection="column" gap={"4px"}>
47
- <HeaderTitle>Deposit</HeaderTitle>
48
- </Box>
49
-
50
- {handleClose && (
51
- <IconButton
52
- onClick={handleClose}
53
- width={"40px"}
54
- marginLeft={"auto"}
55
- >
56
- <Icon
57
- as={X}
58
- color="gray"
59
- width={"16px"}
60
- height={"16px"}
61
- />
62
- </IconButton>
63
- )}
64
- </HeaderWrapper>
65
- </Modal.Header>
66
-
67
- <Modal.Body>
68
- <BodyWrapper>
21
+ <BodyWrapper>
69
22
  <Flex
70
23
  flexDirection={"column"}
71
24
  gap={"16px"}
@@ -113,10 +66,8 @@ const QuoteStep = () => {
113
66
  {routeLoading ? "Loading quote" : "Continue"}
114
67
  </Button>
115
68
  )}
116
- </BodyWrapper>
117
- </Modal.Body>
118
- </>
69
+ </BodyWrapper>
119
70
  );
120
71
  };
121
72
 
122
- export default QuoteStep;
73
+ export default WalletQuoteStep;
@@ -0,0 +1,73 @@
1
+ import { Box, Skeleton } from "@chakra-ui/react";
2
+ import { ListWrapper, BodyWrapper } from "@/components/ui/styled";
3
+ import { useEffect } from "react";
4
+ import { Button } from "@/components/ui";
5
+ import { AssetCard } from "@/components/cards";
6
+ import { useAppStore } from "@/store";
7
+ import { useWalletBalance } from "@/enso-api/api";
8
+ import { formatNumber, formatUSD, normalizeValue } from "@/util";
9
+
10
+ const WalletAssetStep = ({ setStep }: { setStep: (step: string) => void }) => {
11
+ const setIsCheckout = useAppStore((state) => state.setActiveExchange);
12
+ const setTokenIn = useAppStore((state) => state.setTokenIn);
13
+ const tokenIn = useAppStore((state) => state.tokenIn);
14
+ const setChainIdIn = useAppStore((state) => state.setChainIdIn);
15
+ const chainIdIn = useAppStore((state) => state.chainIdIn);
16
+ const { holdingsList, total, isLoading } = useWalletBalance();
17
+
18
+ useEffect(() => {
19
+ setIsCheckout(false);
20
+ }, []);
21
+
22
+ return (
23
+ <BodyWrapper>
24
+ <Box overflowY={"scroll"} maxH={"400px"}>
25
+ <ListWrapper>
26
+ {isLoading
27
+ ? Array.from({ length: 10 }).map((_, index) => (
28
+ <Skeleton
29
+ key={index}
30
+ height="40px"
31
+ width="300px"
32
+ />
33
+ ))
34
+ : holdingsList?.map((asset) => (
35
+ <AssetCard
36
+ key={`${asset.token}-${asset.chainId}`}
37
+ chainId={asset.chainId}
38
+ icon={asset.logoUri}
39
+ title={asset.name}
40
+ balance={`${formatNumber(
41
+ normalizeValue(
42
+ asset.amount,
43
+ asset.decimals,
44
+ ),
45
+ )} ${asset.symbol}`}
46
+ usdBalance={formatUSD(asset.total)}
47
+ tag={""}
48
+ loading={false}
49
+ selected={
50
+ asset.token === tokenIn &&
51
+ asset.chainId === chainIdIn
52
+ }
53
+ onClick={() => {
54
+ setTokenIn(asset.token);
55
+ setChainIdIn(asset.chainId);
56
+ }}
57
+ />
58
+ ))}
59
+ </ListWrapper>
60
+ </Box>
61
+ <Button
62
+ onClick={() => {
63
+ setStep("selectAmount");
64
+ }}
65
+ disabled={!tokenIn}
66
+ >
67
+ Continue
68
+ </Button>
69
+ </BodyWrapper>
70
+ );
71
+ };
72
+
73
+ export default WalletAssetStep;
@@ -65,6 +65,54 @@ export const useWalletBalance = () => {
65
65
  return { holdingsList, total, isLoading };
66
66
  };
67
67
 
68
+ export const useSmartAccountBalance = (smartAccountAddress?: Address) => {
69
+ const config = useConfig();
70
+ const ensoApiToken = useAppStore((state) => state.ensoApiToken);
71
+
72
+ const supportedChainIds = useMemo(
73
+ () => new Set(config.chains.map((chain) => chain.id)),
74
+ [config.chains],
75
+ );
76
+
77
+ const { data, isLoading } = useWalletControllerWalletBalances(
78
+ {
79
+ eoaAddress: smartAccountAddress,
80
+ // @ts-ignore
81
+ chainId: "all",
82
+ },
83
+ {
84
+ query: {
85
+ enabled: !!smartAccountAddress && !!ensoApiToken,
86
+ },
87
+ },
88
+ );
89
+
90
+ const { holdingsList, total } = useMemo(() => {
91
+ if (!data?.length) return { holdingsList: [], total: 0 };
92
+
93
+ const holdingsList = data
94
+ ?.map((balanace) => ({
95
+ ...balanace,
96
+ total:
97
+ +normalizeValue(balanace.amount, balanace.decimals) *
98
+ +balanace.price,
99
+ }))
100
+ .filter(
101
+ (balance) =>
102
+ balance.total > 1 &&
103
+ balance.total < 10000 &&
104
+ supportedChainIds.has(balance.chainId),
105
+ )
106
+ .sort((a, b) => b.total - a.total);
107
+
108
+ const total = holdingsList?.reduce((acc, { total }) => acc + total, 0);
109
+
110
+ return { holdingsList, total };
111
+ }, [data, supportedChainIds]);
112
+
113
+ return { holdingsList, total, isLoading };
114
+ };
115
+
68
116
  const mapTokenData = (tokenData: any): Token => ({
69
117
  ...tokenData,
70
118
  logoURI: tokenData?.logosUri?.[0],
package/src/store.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { create } from "zustand";
2
- import { DEFAULT_SLIPPAGE } from "./util/constants";
3
2
  import { AccessTokenPayload } from "@meshconnect/web-link-sdk";
3
+ import { DEFAULT_SLIPPAGE } from "./util/constants";
4
4
 
5
5
  type Store = {
6
- isCheckout: boolean;
7
- setIsCheckout: (isCheckout: boolean) => void;
6
+ // Exchange flow
7
+ activeExchange: string;
8
+ setActiveExchange: (activeExchange: string) => void;
8
9
 
9
10
  ensoApiToken: string;
10
11
  setEnsoApiToken: (ensoApiToken: string) => void;
@@ -35,8 +36,8 @@ type Store = {
35
36
  };
36
37
 
37
38
  export const useAppStore = create<Store>((set) => ({
38
- isCheckout: false,
39
- setIsCheckout: (isCheckout: boolean) => set({ isCheckout }),
39
+ activeExchange: "",
40
+ setActiveExchange: (activeExchange: string) => set({ activeExchange }),
40
41
 
41
42
  ensoApiToken: "",
42
43
  setEnsoApiToken: (ensoApiToken: string) => set({ ensoApiToken }),
@@ -310,7 +310,8 @@ export const useEtherscanUrl = (
310
310
  if (address) return `${chainPrefix}${type}/${address}`;
311
311
  };
312
312
 
313
- export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
313
+ export const capitalize = (s: string) =>
314
+ s.charAt(0)?.toUpperCase() + s.slice(1);
314
315
 
315
316
  export const getChainName = (chainId: SupportedChainId) => {
316
317
  const geckoName = GECKO_CHAIN_NAMES[chainId];
@@ -1,13 +1,75 @@
1
- import { useAccount, useWalletClient } from "wagmi";
1
+ import { useAccount, useWalletClient, useReadContract } from "wagmi";
2
2
  import { useCapabilities, useSendCalls } from "wagmi/experimental";
3
- import { Address } from "viem";
3
+ import { Address, parseAbiItem } from "viem";
4
4
  import { useCallback, useMemo } from "react";
5
- import { useEnsoToken, useEnsoPrice, useEnsoData } from "@/enso-api/api";
5
+ import {
6
+ useEnsoToken,
7
+ useEnsoPrice,
8
+ useEnsoData,
9
+ useSmartAccountBalance,
10
+ } from "@/enso-api/api";
6
11
  import { getWalletDisplayName, getWalletIcon } from "@/util/wallet";
7
12
  import { formatUSD, normalizeValue } from "@/util";
8
13
  import { useAppStore } from "@/store";
9
14
  import { VITALIK_ADDRESS } from "./constants";
10
15
 
16
+ export function getERC4337CloneFactory(chainId: ChainIds): AddressArg {
17
+ return "0x1a59347d28f64091079fa04a2cbd03da63dff154";
18
+ }
19
+
20
+ const GET_SMART_ADDRESS = parseAbiItem(
21
+ "function getAddress(address) external view returns (address)",
22
+ );
23
+
24
+ export const useSmartAccountAddress = (
25
+ ownerAddress?: Address,
26
+ chainId?: number,
27
+ ) => {
28
+ const factoryAddress = getERC4337CloneFactory(chainId);
29
+
30
+ const {
31
+ data: smartAccountAddress,
32
+ isLoading,
33
+ error,
34
+ } = useReadContract({
35
+ address: factoryAddress as Address,
36
+ abi: [GET_SMART_ADDRESS],
37
+ functionName: "getAddress",
38
+ args: ownerAddress ? [ownerAddress] : undefined,
39
+ chainId,
40
+ query: {
41
+ enabled: !!(factoryAddress && ownerAddress && chainId),
42
+ },
43
+ });
44
+
45
+ return {
46
+ smartAccountAddress: smartAccountAddress as Address | undefined,
47
+ isLoading,
48
+ };
49
+ };
50
+
51
+ export const useSmartAccountBalances = (chainId: number = 1) => {
52
+ const { address } = useAccount();
53
+
54
+ // Get smart account address for the given chain
55
+ const { smartAccountAddress, isLoading: isLoadingSmartAccount } =
56
+ useSmartAccountAddress(address, chainId);
57
+
58
+ // Get balances from smart account
59
+ const {
60
+ holdingsList,
61
+ total,
62
+ isLoading: isLoadingBalances,
63
+ } = useSmartAccountBalance(smartAccountAddress);
64
+
65
+ return {
66
+ smartAccountAddress,
67
+ holdingsList,
68
+ total,
69
+ isLoading: isLoadingSmartAccount || isLoadingBalances,
70
+ };
71
+ };
72
+
11
73
  export const useAppDetails = () => {
12
74
  const { address = VITALIK_ADDRESS } = useAccount();
13
75
  const amountIn = useAppStore((state) => state.amountIn);
@@ -16,7 +78,7 @@ export const useAppDetails = () => {
16
78
  const slippage = useAppStore((state) => state.slippage);
17
79
  const chainIdIn = useAppStore((state) => state.chainIdIn);
18
80
  const chainIdOut = useAppStore((state) => state.chainIdOut);
19
- const isCheckout = useAppStore((state) => state.isCheckout);
81
+ const isCheckout = useAppStore((state) => state.activeExchange);
20
82
 
21
83
  const {
22
84
  data: [tokenInData],
@@ -59,7 +121,7 @@ export const useSendTxns = ({
59
121
  // const useFallback = !atomicOkay; // one-line policy; tweak to taste
60
122
  const wallet = useWalletClient();
61
123
 
62
- console.log(caps, atomicOkay);
124
+ console.log(caps, atomicOkay, wallet.data);
63
125
 
64
126
  const sendTxns = useCallback(
65
127
  async (
@@ -1,25 +1,11 @@
1
- import {
2
- ReactElement,
3
- ReactNode,
4
- useCallback,
5
- useEffect,
6
- useState,
7
- } from "react";
8
- import {
9
- useAccount,
10
- useBalance,
11
- useReadContract,
12
- useSendTransaction,
13
- UseSimulateContractParameters,
14
- } from "wagmi";
1
+ import { ReactElement, useCallback, useEffect, useState } from "react";
2
+ import { useAccount, useBalance, useReadContract } from "wagmi";
15
3
  import { Address, isAddress, erc20Abi } from "viem";
16
4
  import { WalletIcon } from "lucide-react";
17
5
  import { useQueryClient } from "@tanstack/react-query";
18
- import { RouteData } from "@ensofinance/sdk";
19
6
  import { usePriorityChainId } from "./common";
20
7
  import { ETH_ADDRESS, SupportedChainId } from "@/util/constants";
21
8
  import { compareCaseInsensitive } from "@/util/index";
22
- import { toaster } from "@/components/ui/toaster";
23
9
 
24
10
  import metamaskIcon from "@/assets/metamask.png";
25
11
  import rabbyIcon from "@/assets/rabby.png";
@@ -139,40 +125,6 @@ export const useAllowance = (token: Address, spender: Address) => {
139
125
  };
140
126
  };
141
127
 
142
- export const useExtendedSendTransaction = ({
143
- title,
144
- args,
145
- onSuccess,
146
- crosschain,
147
- }: {
148
- title: string;
149
- args: UseSimulateContractParameters;
150
- onSuccess?: (hash: string) => void;
151
- crosschain?: boolean;
152
- }) => {
153
- const sendTransaction = useSendTransaction();
154
-
155
- const send = useCallback(() => {
156
- sendTransaction.sendTransaction(args, {
157
- onError: (error) => {
158
- toaster.create({
159
- title: "Error",
160
- // @ts-ignore
161
- description: error?.cause?.shortMessage || error.message,
162
- type: "error",
163
- });
164
- console.error(error);
165
- },
166
- onSuccess,
167
- });
168
- }, [sendTransaction, args, onSuccess]);
169
-
170
- return {
171
- ...sendTransaction,
172
- send,
173
- };
174
- };
175
-
176
128
  export const useIsApproveNeeded = (
177
129
  tokenIn: Address,
178
130
  target: Address = "0xF75584eF6673aD213a685a1B58Cc0330B8eA22Cf",
@@ -193,25 +145,6 @@ export const useIsApproveNeeded = (
193
145
  };
194
146
  };
195
147
 
196
- export const useSendEnsoTransaction = ({
197
- args,
198
- title,
199
- crosschain,
200
- onSuccess,
201
- }: {
202
- args: RouteData["tx"];
203
- title: string;
204
- crosschain?: boolean;
205
- onSuccess?: (hash: string) => void;
206
- }) => {
207
- return useExtendedSendTransaction({
208
- title,
209
- args,
210
- crosschain,
211
- onSuccess,
212
- });
213
- };
214
-
215
148
  /**
216
149
  * Get the appropriate wallet icon based on the connector name
217
150
  * @param connectorName - The name of the wallet connector