Skip to content

Commit 4d8e7e5

Browse files
committed
refactor: cleaned up swap by using react context as store,
bug-fix: double scroll wheel
1 parent f2c75f6 commit 4d8e7e5

File tree

13 files changed

+454
-299
lines changed

13 files changed

+454
-299
lines changed

packages/wallet-widget/src/components/Select/NetworkSelect.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { useState } from 'react'
33
import { useChainId, useChains, useSwitchChain } from 'wagmi'
44

55
import { NetworkRow } from '../Filter/NetworkRow'
6-
import { WALLET_HEIGHT } from '../SequenceWalletProvider'
76

87
import { SlideupDrawer } from './SlideupDrawer'
98

@@ -39,7 +38,7 @@ export const NetworkSelect = () => {
3938
<ChevronUpDownIcon className="text-muted" />
4039
{isOpen && (
4140
<SlideupDrawer label="Network" onClose={() => setIsOpen(false)}>
42-
<div className="flex flex-col gap-2 px-2" style={{ maxHeight: `calc(${WALLET_HEIGHT} / 2)`, overflowY: 'auto' }}>
41+
<div className="flex flex-col gap-2" style={{ overflowY: 'auto' }}>
4342
{chains.map(chain => (
4443
<NetworkRow
4544
key={chain.id}

packages/wallet-widget/src/components/Select/SelectWalletRow.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Text } from '@0xsequence/design-system'
33

44
import { useSettings } from '../../hooks'
55
import { useFiatWalletsMap } from '../../hooks/useFiatWalletsMap'
6-
76
import { CopyButton } from '../CopyButton'
87
import { ListCardSelect } from '../ListCard/ListCardSelect'
98
import { WalletAccountGradient } from '../WalletAccountGradient'

packages/wallet-widget/src/components/Select/WalletSelect.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import { ChevronUpDownIcon, Text } from '@0xsequence/design-system'
33
import { useState } from 'react'
44

55
import { SelectWalletRow } from './SelectWalletRow'
6-
import { WALLET_HEIGHT } from '../SequenceWalletProvider'
7-
86
import { SlideupDrawer } from './SlideupDrawer'
97

108
const WALLET_SELECT_HEIGHT = 60
@@ -37,7 +35,7 @@ export const WalletSelect = ({ selectedWallet, onClick }: { selectedWallet: stri
3735
<ChevronUpDownIcon className="text-muted" />
3836
{isOpen && (
3937
<SlideupDrawer label="Network" onClose={() => setIsOpen(false)}>
40-
<div className="flex flex-col gap-2 px-2" style={{ maxHeight: `calc(${WALLET_HEIGHT} / 2)`, overflowY: 'auto' }}>
38+
<div className="flex flex-col gap-2" style={{ overflowY: 'auto' }}>
4139
{allButActiveWallet.map(wallet => (
4240
<SelectWalletRow
4341
key={wallet.address}
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import { SwapQuote } from '@0xsequence/api'
2+
import { sendTransactions } from '@0xsequence/connect'
3+
import { compareAddress, useToast } from '@0xsequence/design-system'
4+
import { useAPIClient, useIndexerClient } from '@0xsequence/hooks'
5+
import { ReactNode, useEffect, useState } from 'react'
6+
import { Hex, zeroAddress } from 'viem'
7+
import { useAccount, useChainId, usePublicClient, useWalletClient } from 'wagmi'
8+
9+
import { SwapContextProvider } from '../../../contexts/Swap'
10+
import { useNavigation } from '../../../hooks/useNavigation'
11+
import { TokenBalanceWithPrice } from '../../../utils'
12+
13+
export const SwapProvider = ({ children }: { children: ReactNode }) => {
14+
const toast = useToast()
15+
const { address: userAddress, connector } = useAccount()
16+
const { setNavigation } = useNavigation()
17+
const apiClient = useAPIClient()
18+
const connectedChainId = useChainId()
19+
20+
const [fromCoin, _setFromCoin] = useState<TokenBalanceWithPrice>()
21+
const [fromAmount, setFromAmount] = useState<number>(0)
22+
const [toCoin, _setToCoin] = useState<TokenBalanceWithPrice>()
23+
const [toAmount, setToAmount] = useState<number>(0)
24+
const [recentInput, setRecentInput] = useState<'from' | 'to'>('from')
25+
const [setNonRecentAmount, _setNonRecentAmount] = useState<() => void>(() => 0)
26+
27+
const [isSwapReady, setIsSwapReady] = useState(false)
28+
const [swapQuoteData, setSwapQuoteData] = useState<SwapQuote>()
29+
const [isSwapQuotePending, setIsSwapQuotePending] = useState(false)
30+
const [hasInsufficientFunds, setHasInsufficientFunds] = useState(false)
31+
const [isErrorSwapQuote, setIsErrorSwapQuote] = useState(false)
32+
33+
const [isTxnPending, setIsTxnPending] = useState(false)
34+
const [isErrorTxn, setIsErrorTxn] = useState(false)
35+
36+
const publicClient = usePublicClient({ chainId: connectedChainId })
37+
const { data: walletClient } = useWalletClient({ chainId: connectedChainId })
38+
const indexerClient = useIndexerClient(connectedChainId)
39+
40+
useEffect(() => {
41+
setFromCoin(undefined)
42+
setFromAmount(0)
43+
setToCoin(undefined)
44+
setToAmount(0)
45+
setRecentInput('from')
46+
setIsSwapReady(false)
47+
setSwapQuoteData(undefined)
48+
setIsSwapQuotePending(false)
49+
setIsErrorSwapQuote(false)
50+
setIsTxnPending(false)
51+
setIsErrorTxn(false)
52+
}, [userAddress, connectedChainId])
53+
54+
useEffect(() => {
55+
if (recentInput === 'from') {
56+
_setNonRecentAmount(() => setFromAmount)
57+
} else {
58+
_setNonRecentAmount(() => setToAmount)
59+
}
60+
}, [recentInput])
61+
62+
useEffect(() => {
63+
setIsSwapReady(false)
64+
setSwapQuoteData(undefined)
65+
setIsErrorSwapQuote(false)
66+
}, [fromCoin, toCoin, fromAmount, toAmount])
67+
68+
useEffect(() => {
69+
const fetchSwapQuote = async () => {
70+
if (!fromCoin || !toCoin || (fromAmount === 0 && toAmount === 0)) {
71+
return
72+
}
73+
74+
setIsSwapQuotePending(true)
75+
setIsErrorSwapQuote(false)
76+
77+
let swapQuote
78+
try {
79+
// swapQuote = await apiClient.getSwapQuoteV2({
80+
// userAddress: String(userAddress),
81+
// buyCurrencyAddress: toCoin.contractAddress,
82+
// sellCurrencyAddress: fromCoin.contractAddress,
83+
// tokenAmount: String(recentInput === 'to' ? toAmount : fromAmount),
84+
// isBuyAmount: recentInput === 'to',
85+
// chainId: connectedChainId,
86+
// includeApprove: true
87+
// })
88+
89+
swapQuote = await apiClient.getSwapQuoteV2({
90+
userAddress: String(userAddress),
91+
buyCurrencyAddress: toCoin.contractAddress,
92+
sellCurrencyAddress: fromCoin.contractAddress,
93+
buyAmount: String(toAmount),
94+
chainId: connectedChainId,
95+
includeApprove: true
96+
})
97+
98+
const transactionValue = swapQuote?.swapQuote?.transactionValue || '0'
99+
100+
// TODO: change this to "amount" from return
101+
102+
if (recentInput === 'from') {
103+
setToAmount(Number(transactionValue))
104+
} else {
105+
setFromAmount(Number(transactionValue))
106+
}
107+
108+
setSwapQuoteData(swapQuote?.swapQuote)
109+
setIsSwapReady(true)
110+
} catch (error) {
111+
const hasInsufficientFunds = (error as any).code === -4
112+
setHasInsufficientFunds(hasInsufficientFunds)
113+
setIsErrorSwapQuote(true)
114+
}
115+
setIsSwapQuotePending(false)
116+
}
117+
118+
fetchSwapQuote()
119+
}, [fromCoin, toCoin, fromAmount, toAmount])
120+
121+
const setFromCoin = (coin: TokenBalanceWithPrice | undefined) => {
122+
if (coin?.chainId === toCoin?.chainId && coin?.contractAddress === toCoin?.contractAddress) {
123+
switchCoinOrder()
124+
} else {
125+
_setFromCoin(coin)
126+
}
127+
}
128+
129+
const setToCoin = (coin: TokenBalanceWithPrice | undefined) => {
130+
if (coin?.chainId === fromCoin?.chainId && coin?.contractAddress === fromCoin?.contractAddress) {
131+
switchCoinOrder()
132+
} else {
133+
_setToCoin(coin)
134+
}
135+
}
136+
137+
const switchCoinOrder = () => {
138+
const tempFrom = fromCoin
139+
const tempTo = toCoin
140+
const tempFromAmount = fromAmount
141+
const tempToAmount = toAmount
142+
_setFromCoin(tempTo)
143+
_setToCoin(tempFrom)
144+
setFromAmount(tempToAmount)
145+
setToAmount(tempFromAmount)
146+
}
147+
148+
const onSubmitSwap = async () => {
149+
if (isErrorSwapQuote || !userAddress || !publicClient || !walletClient || !connector) {
150+
console.error('Please ensure validation before submitting')
151+
return
152+
}
153+
154+
setIsErrorTxn(false)
155+
setIsTxnPending(true)
156+
157+
try {
158+
const isSwapNativeToken = compareAddress(zeroAddress, swapQuoteData?.currencyAddress || '')
159+
160+
const getSwapTransactions = () => {
161+
if (!swapQuoteData) {
162+
return []
163+
}
164+
165+
const swapTransactions = [
166+
// Swap quote optional approve step
167+
...(swapQuoteData?.approveData && !isSwapNativeToken
168+
? [
169+
{
170+
to: swapQuoteData?.currencyAddress as Hex,
171+
data: swapQuoteData?.approveData as Hex,
172+
chain: connectedChainId
173+
}
174+
]
175+
: []),
176+
// Swap quote tx
177+
{
178+
to: swapQuoteData?.to as Hex,
179+
data: swapQuoteData?.transactionData as Hex,
180+
chain: connectedChainId,
181+
...(isSwapNativeToken
182+
? {
183+
value: BigInt(swapQuoteData?.transactionValue || '0')
184+
}
185+
: {})
186+
}
187+
]
188+
return swapTransactions
189+
}
190+
191+
const walletClientChainId = await walletClient.getChainId()
192+
if (walletClientChainId !== connectedChainId) {
193+
await walletClient.switchChain({ id: connectedChainId })
194+
}
195+
196+
await sendTransactions({
197+
connector,
198+
walletClient,
199+
publicClient,
200+
chainId: connectedChainId,
201+
indexerClient,
202+
senderAddress: userAddress,
203+
transactions: [...getSwapTransactions()]
204+
})
205+
206+
toast({
207+
title: 'Transaction sent',
208+
description: `Successfully swapped ${fromAmount} ${fromCoin?.contractInfo?.name} for ${toAmount} ${toCoin?.contractInfo?.name}`,
209+
variant: 'success'
210+
})
211+
212+
setNavigation({
213+
location: 'home'
214+
})
215+
} catch (error) {
216+
console.error('Failed to send transactions', error)
217+
setIsSwapReady(false)
218+
setIsTxnPending(false)
219+
setIsErrorTxn(true)
220+
}
221+
}
222+
223+
return (
224+
<SwapContextProvider
225+
value={{
226+
fromCoin,
227+
fromAmount,
228+
toCoin,
229+
toAmount,
230+
recentInput,
231+
isSwapReady,
232+
isSwapQuotePending,
233+
hasInsufficientFunds,
234+
isErrorSwapQuote,
235+
isTxnPending,
236+
isErrorTxn,
237+
setFromCoin,
238+
setFromAmount,
239+
setToCoin,
240+
setToAmount,
241+
setRecentInput,
242+
setNonRecentAmount,
243+
switchCoinOrder,
244+
onSubmitSwap
245+
}}
246+
>
247+
{children}
248+
</SwapContextProvider>
249+
)
250+
}

packages/wallet-widget/src/components/SequenceWalletProvider/SequenceWalletProvider.tsx

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { History, Navigation, NavigationContextProvider, WalletModalContextProvi
1212
import { WalletContentRefProvider, WalletContentRefContext } from '../../contexts/WalletContentRef'
1313

1414
import { FiatWalletsMapProvider } from './ProviderComponents/FiatWalletsMapProvider'
15+
import { SwapProvider } from './ProviderComponents/SwapProvider'
1516
import { getHeader, getContent } from './utils'
1617

1718
export const WALLET_WIDTH = '460px'
@@ -94,36 +95,38 @@ export const WalletContent = ({ children }: SequenceWalletProviderProps) => {
9495
<NavigationContextProvider value={{ setHistory, history, isBackButtonEnabled, setIsBackButtonEnabled }}>
9596
<FiatWalletsMapProvider>
9697
<ToastProvider>
97-
<ShadowRoot theme={theme}>
98-
<AnimatePresence>
99-
{openWalletModal && !isAddFundsModalOpen && !isConnectModalOpen && (
100-
<Modal
101-
contentProps={{
102-
style: {
103-
maxWidth: WALLET_WIDTH,
104-
height: 'fit-content',
105-
...getModalPositionCss(position),
106-
scrollbarColor: 'gray black',
107-
scrollbarWidth: 'thin'
108-
}
109-
}}
110-
scroll={false}
111-
onClose={() => setOpenWalletModal(false)}
112-
>
113-
<div id="sequence-kit-wallet-content" ref={walletContentRef}>
114-
{getHeader(navigation)}
115-
116-
{displayScrollbar ? (
117-
<Scroll style={{ paddingTop: paddingTop, height: 'min(800px, 90vh)' }}>{getContent(navigation)}</Scroll>
118-
) : (
119-
getContent(navigation)
120-
)}
121-
</div>
122-
</Modal>
123-
)}
124-
</AnimatePresence>
125-
</ShadowRoot>
126-
{children}
98+
<SwapProvider>
99+
<ShadowRoot theme={theme}>
100+
<AnimatePresence>
101+
{openWalletModal && !isAddFundsModalOpen && !isConnectModalOpen && (
102+
<Modal
103+
contentProps={{
104+
style: {
105+
maxWidth: WALLET_WIDTH,
106+
height: 'fit-content',
107+
...getModalPositionCss(position),
108+
scrollbarColor: 'gray black',
109+
scrollbarWidth: 'thin'
110+
}
111+
}}
112+
scroll={false}
113+
onClose={() => setOpenWalletModal(false)}
114+
>
115+
<div id="sequence-kit-wallet-content" ref={walletContentRef}>
116+
{getHeader(navigation)}
117+
118+
{displayScrollbar ? (
119+
<Scroll style={{ paddingTop: paddingTop, height: 'min(800px, 90vh)' }}>{getContent(navigation)}</Scroll>
120+
) : (
121+
getContent(navigation)
122+
)}
123+
</div>
124+
</Modal>
125+
)}
126+
</AnimatePresence>
127+
</ShadowRoot>
128+
{children}
129+
</SwapProvider>
127130
</ToastProvider>
128131
</FiatWalletsMapProvider>
129132
</NavigationContextProvider>

packages/wallet-widget/src/components/WalletHeader/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { useState } from 'react'
55

66
import { HEADER_HEIGHT, HEADER_HEIGHT_WITH_LABEL } from '../../constants'
77
import { useNavigation } from '../../hooks'
8-
import { SlideupDrawer } from '../Select/SlideupDrawer'
98
import { SelectWalletRow } from '../Select/SelectWalletRow'
9+
import { SlideupDrawer } from '../Select/SlideupDrawer'
1010

1111
import { AccountInformation } from './components/AccountInformation'
1212

0 commit comments

Comments
 (0)