Skip to content

Commit 283d07b

Browse files
authored
Refactor: Decompose swap form (#200)
### Description While trying to debug, I got very annoyed with the unwieldiness of the SwapForm with lots of components and hooks inlined in the same file. This PR is about decomposing the SwapForm into smaller parts. It extracts components and hooks into individual files and aims at making the DX for this feature better. ### Other changes - I also upgraded yarn from 3.3.1 to 4.6.0 while I was at it, hence the `yarn.lock` update, but no dependencies where actually changed or updated. - I also spend some time improving the UX of the submit button on the SwapConfirm page and make sure it always displays the right text and the right enabled/disabled status ### Tested I checked that swaps still work in both directions and with or without approvals. ### How to review Everything should work exactly as it does on production right now, no functionality should have changed. - [ ] Check out the preview URL - [ ] Do a few swaps in different directions and with and without approval TXs - [ ] Review the code
1 parent 699ec1a commit 283d07b

25 files changed

+4971
-4641
lines changed

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
node_modules
22
src/vendor
33
dist
4+
out
45
webpack.config.js
56
jest.config.js
67
tailwind.config.js

.yarn/releases/yarn-3.3.1.cjs

-823
This file was deleted.

.yarn/releases/yarn-4.6.0.cjs

+934
Large diffs are not rendered by default.

.yarnrc.yml

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
compressionLevel: mixed
2+
3+
enableGlobalCache: false
4+
15
nodeLinker: node-modules
26

37
plugins:
48
- path: .yarn/plugins/@yarnpkg/plugin-outdated.cjs
59
spec: "https://mskelton.dev/yarn-outdated/v3"
610

7-
yarnPath: .yarn/releases/yarn-3.3.1.cjs
11+
yarnPath: .yarn/releases/yarn-4.6.0.cjs

package.json

+8-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@
1414
},
1515
"homepage": "https://app.mento.org",
1616
"license": "Apache-2.0",
17+
"engines": {
18+
"node": "^18.20.6"
19+
},
20+
"packageManager": "[email protected]",
1721
"scripts": {
18-
"dev": "next",
22+
"dev": "NODE_ENV=development next",
1923
"build": "next build",
2024
"typecheck": "tsc",
2125
"start": "next start",
@@ -80,5 +84,7 @@
8084
"bignumber.js": "9.1.1",
8185
"@types/react": "18.0.34"
8286
},
83-
"packageManager": "[email protected]"
87+
"volta": {
88+
"node": "18.20.7"
89+
}
8490
}

src/config/tokens.ts

+25-18
Original file line numberDiff line numberDiff line change
@@ -203,33 +203,40 @@ export function isNativeStableToken(tokenId: string) {
203203
return NativeStableTokenIds.includes(tokenId as TokenId)
204204
}
205205

206-
export async function isSwappable(token_1: string, token_2: string, chainId: number) {
206+
export async function isSwappable(token1: string, token2: string, chainId: number) {
207+
// Exit early if the same token was passed in two times
208+
if (token1 === token2) return false
209+
207210
const sdk = await getMentoSdk(chainId)
208211
const tradablePairs = await sdk.getTradablePairs()
209212
if (!tradablePairs) return false
210-
if (token_1 === token_2) return false
213+
214+
const token1Address = getTokenAddress(token1 as TokenId, chainId)
215+
const token2Address = getTokenAddress(token2 as TokenId, chainId)
211216

212217
return tradablePairs.some(
213-
(assets) =>
214-
assets.find((asset) => asset.address === getTokenAddress(token_1 as TokenId, chainId)) &&
215-
assets.find((asset) => asset.address === getTokenAddress(token_2 as TokenId, chainId))
218+
(pair) =>
219+
pair.find((asset) => asset.address === token1Address) &&
220+
pair.find((asset) => asset.address === token2Address)
216221
)
217222
}
218223

219-
export async function getSwappableTokenOptions(token: string, chainId: ChainId) {
220-
const options = getTokenOptionsByChainId(chainId)
224+
export async function getSwappableTokenOptions(inputTokenId: string, chainId: ChainId) {
225+
// Get all available tokens for the chain except the input token
226+
const tokenOptions = getTokenOptionsByChainId(chainId).filter(
227+
(tokenId) => tokenId !== inputTokenId
228+
)
229+
230+
// Check swappability in parallel and maintain order
231+
const swappableTokens = await Promise.all(
232+
tokenOptions.map(async (tokenId) => {
233+
const swappable = await isSwappable(tokenId, inputTokenId, chainId)
234+
return swappable ? tokenId : null
235+
})
236+
)
221237

222-
const swappableOptions = await Promise.all(
223-
options.map(async (tkn) => ({
224-
token: tkn,
225-
isSwappable: await isSwappable(tkn, token, chainId),
226-
}))
227-
).then((results) => {
228-
return results
229-
.filter((result) => result.isSwappable && result.token !== token)
230-
.map((result) => result.token)
231-
})
232-
return swappableOptions
238+
// Filter out non-swappable tokens (null values)
239+
return swappableTokens.filter((tokenId): tokenId is TokenId => tokenId !== null)
233240
}
234241

235242
export function getTokenOptionsByChainId(chainId: ChainId): TokenId[] {

src/features/swap/SwapConfirm.tsx

+26-21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import BigNumber from 'bignumber.js'
21
import Lottie from 'lottie-react'
32
import { SVGProps, useEffect, useState } from 'react'
43
import mentoLoaderBlue from 'src/animations/Mentoloader_blue.json'
@@ -8,13 +7,6 @@ import { Button3D } from 'src/components/buttons/3DButton'
87
import { Tooltip } from 'src/components/tooltip/Tooltip'
98
import { TokenId, Tokens } from 'src/config/tokens'
109
import { useAppDispatch, useAppSelector } from 'src/features/store/hooks'
11-
import { setConfirmView, setFormValues } from 'src/features/swap/swapSlice'
12-
import { SwapFormValues } from 'src/features/swap/types'
13-
import { useAllowance } from 'src/features/swap/useAllowance'
14-
import { useApproveTransaction } from 'src/features/swap/useApproveTransaction'
15-
import { useSwapQuote } from 'src/features/swap/useSwapQuote'
16-
import { useSwapTransaction } from 'src/features/swap/useSwapTransaction'
17-
import { getMaxSellAmount, getMinBuyAmount } from 'src/features/swap/utils'
1810
import { TokenIcon } from 'src/images/tokens/TokenIcon'
1911
import { FloatingBox } from 'src/layout/FloatingBox'
2012
import { Modal } from 'src/layout/Modal'
@@ -23,6 +15,15 @@ import { logger } from 'src/utils/logger'
2315
import { truncateTextByLength } from 'src/utils/string'
2416
import { useAccount, useChainId } from 'wagmi'
2517

18+
import { useApproveTransaction } from './hooks/useApproveTransaction'
19+
import { useSwapAllowance } from './hooks/useSwapAllowance'
20+
import { useSwapQuote } from './hooks/useSwapQuote'
21+
import { useSwapState } from './hooks/useSwapState'
22+
import { useSwapTransaction } from './hooks/useSwapTransaction'
23+
import { setConfirmView, setFormValues } from './swapSlice'
24+
import type { SwapFormValues } from './types'
25+
import { getMaxSellAmount, getMinBuyAmount } from './utils'
26+
2627
interface Props {
2728
formValues: SwapFormValues
2829
}
@@ -95,17 +96,13 @@ export function SwapConfirmCard({ formValues }: Props) {
9596
)
9697
const [isApproveConfirmed, setApproveConfirmed] = useState(false)
9798

98-
const { allowance, isLoading: isAllowanceLoading } = useAllowance(
99+
const { skipApprove, isAllowanceLoading } = useSwapAllowance({
99100
chainId,
100101
fromTokenId,
101102
toTokenId,
102-
address
103-
)
104-
const needsApproval = !isAllowanceLoading && new BigNumber(allowance).lte(approveAmount)
105-
const skipApprove = !isAllowanceLoading && !needsApproval
106-
107-
logger.info(`Allowance loading: ${isAllowanceLoading}`)
108-
logger.info(`Needs approval: ${needsApproval}`)
103+
approveAmount,
104+
address,
105+
})
109106

110107
useEffect(() => {
111108
if (skipApprove) {
@@ -129,11 +126,10 @@ export function SwapConfirmCard({ formValues }: Props) {
129126
const onSubmit = async () => {
130127
if (!rate || !amountWei || !address || !isConnected) return
131128

132-
setIsModalOpen(true)
133-
134129
if (skipApprove && sendSwapTx) {
135130
try {
136131
logger.info('Skipping approve, sending swap tx directly')
132+
setIsModalOpen(true)
137133
const swapResult = await sendSwapTx()
138134
const swapReceipt = await swapResult.wait(1)
139135
logger.info(`Tx receipt received for swap: ${swapReceipt?.transactionHash}`)
@@ -150,6 +146,7 @@ export function SwapConfirmCard({ formValues }: Props) {
150146
if (!skipApprove && sendApproveTx) {
151147
try {
152148
logger.info('Sending approve tx')
149+
setIsModalOpen(true)
153150
const approveResult = await sendApproveTx()
154151
const approveReceipt = await approveResult.wait(1)
155152
toastToYourSuccess(
@@ -192,7 +189,15 @@ export function SwapConfirmCard({ formValues }: Props) {
192189
refetch().catch((e) => logger.error('Failed to refetch quote:', e))
193190
}
194191

195-
const isSwapReady = !sendApproveTx || isApproveTxSuccess || isApproveTxLoading
192+
const { text: buttonText, disabled: isButtonDisabled } = useSwapState({
193+
isAllowanceLoading,
194+
skipApprove,
195+
sendApproveTx,
196+
isApproveTxLoading,
197+
isApproveTxSuccess,
198+
sendSwapTx,
199+
fromTokenId,
200+
})
196201

197202
return (
198203
<FloatingBox
@@ -241,8 +246,8 @@ export function SwapConfirmCard({ formValues }: Props) {
241246
</div>
242247

243248
<div className="flex w-full px-6 pb-6 mt-6">
244-
<Button3D isFullWidth onClick={onSubmit} isDisabled={isSwapReady}>
245-
Swap
249+
<Button3D isFullWidth onClick={onSubmit} isDisabled={isButtonDisabled}>
250+
{buttonText}
246251
</Button3D>
247252
</div>
248253
<Modal

0 commit comments

Comments
 (0)