Skip to content

Commit

Permalink
feat: improve zap flow
Browse files Browse the repository at this point in the history
  • Loading branch information
0xMasayoshi committed Nov 19, 2024
1 parent 4b2e47f commit ae128e6
Show file tree
Hide file tree
Showing 9 changed files with 424 additions and 202 deletions.
169 changes: 122 additions & 47 deletions apps/web/src/app/(networks)/(evm)/[chainId]/pool/v2/add/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use client'

import { PlusIcon } from '@heroicons/react-v1/solid'
import { SlippageToleranceStorageKey } from '@sushiswap/hooks'
import { createToast } from '@sushiswap/notifications'
import { Button, Dots, FormSection, Loader } from '@sushiswap/ui'
import { useRouter } from 'next/navigation'
import { notFound } from 'next/navigation'
Expand All @@ -22,10 +24,14 @@ import {
} from 'src/lib/constants'
import { isSushiSwapV2Pool } from 'src/lib/functions'
import { useZap } from 'src/lib/hooks'
import { useSlippageTolerance } from 'src/lib/hooks/useSlippageTolerance'
import { Web3Input } from 'src/lib/wagmi/components/web3-input'
import { SushiSwapV2PoolState } from 'src/lib/wagmi/hooks/pools/hooks/useSushiSwapV2Pools'
import { Checker } from 'src/lib/wagmi/systems/Checker'
import { CheckerProvider } from 'src/lib/wagmi/systems/Checker/Provider'
import {
CheckerProvider,
useApproved,
} from 'src/lib/wagmi/systems/Checker/Provider'
import { PoolFinder } from 'src/lib/wagmi/systems/PoolFinder/PoolFinder'
import { AddSectionPoolShareCardV2 } from 'src/ui/pool/AddSectionPoolShareCardV2'
import { AddSectionReviewModalLegacy } from 'src/ui/pool/AddSectionReviewModalLegacy'
Expand All @@ -46,15 +52,22 @@ import { Amount, Type, tryParseAmount } from 'sushi/currency'
import { ZERO } from 'sushi/math'
import { SushiSwapV2Pool } from 'sushi/pool/sushiswap-v2'
import { SWRConfig } from 'swr'
import { useAccount, useEstimateGas, useSendTransaction } from 'wagmi'
import { SendTransactionReturnType } from 'viem'
import {
useAccount,
useEstimateGas,
usePublicClient,
useSendTransaction,
} from 'wagmi'
import { useRefetchBalances } from '~evm/_common/ui/balance-provider/use-refetch-balances'

export default function Page({ params }: { params: { chainId: string } }) {
const chainId = +params.chainId as ChainId
if (!isSushiSwapV2ChainId(chainId)) {
return notFound()
}

const [useZap, setUseZap] = useState(false)
const [isZapModeEnabled, setIsZapModeEnabled] = useState(false)

const router = useRouter()
const [token0, setToken0] = useState<Type | undefined>(
Expand Down Expand Up @@ -132,9 +145,9 @@ export default function Page({ params }: { params: { chainId: string } }) {
isZapSupportedChainId(chainId) &&
poolState === SushiSwapV2PoolState.EXISTS
) {
setUseZap(true)
setIsZapModeEnabled(true)
} else {
setUseZap(false)
setIsZapModeEnabled(false)
}
}, [poolState])

Expand Down Expand Up @@ -179,9 +192,12 @@ export default function Page({ params }: { params: { chainId: string } }) {
>
{isZapSupportedChainId(chainId) &&
poolState === SushiSwapV2PoolState.EXISTS ? (
<ToggleZapCard checked={useZap} onCheckedChange={setUseZap} />
<ToggleZapCard
checked={isZapModeEnabled}
onCheckedChange={setIsZapModeEnabled}
/>
) : null}
{useZap ? (
{isZapModeEnabled ? (
<ZapWidget
chainId={chainId}
pool={pool}
Expand Down Expand Up @@ -221,13 +237,37 @@ interface ZapWidgetProps {
title: ReactNode
}

const ZapWidget: FC<ZapWidgetProps> = ({ chainId, pool, poolState, title }) => {
const { address } = useAccount()
const ZapWidget: FC<ZapWidgetProps> = (props) => {
return (
<CheckerProvider>
<_ZapWidget {...props} />
</CheckerProvider>
)
}

const _ZapWidget: FC<ZapWidgetProps> = ({
chainId,
pool,
poolState,
title,
}) => {
const client = usePublicClient()

const { address, chain } = useAccount()

const [slippageTolerance] = useSlippageTolerance(
SlippageToleranceStorageKey.AddLiquidity,
)

const [inputAmount, setInputAmount] = useState('')
const [inputCurrency, setInputCurrency] = useState<Type>(
const [inputCurrency, _setInputCurrency] = useState<Type>(
defaultCurrency[chainId as keyof typeof defaultCurrency],
)
const setInputCurrency = useCallback((currency: Type) => {
_setInputCurrency(currency)
setInputAmount('')
}, [])

const parsedInputAmount = useMemo(
() =>
tryParseAmount(inputAmount, inputCurrency) ||
Expand All @@ -241,16 +281,19 @@ const ZapWidget: FC<ZapWidgetProps> = ({ chainId, pool, poolState, title }) => {
tokenIn: inputCurrency.isNative ? NativeAddress : inputCurrency.address,
amountIn: parsedInputAmount?.quotient?.toString(),
tokenOut: pool?.liquidityToken.address,
slippage: slippageTolerance,
})

const { approved } = useApproved(APPROVE_TAG_ZAP_LEGACY)

const { data: estGas, isError: isEstGasError } = useEstimateGas({
chainId,
account: address,
to: zapResponse?.tx.to,
data: zapResponse?.tx.data,
value: zapResponse?.tx.value,
query: {
enabled: Boolean(address && zapResponse?.tx),
enabled: Boolean(approved && address && zapResponse?.tx),
},
})

Expand All @@ -260,7 +303,41 @@ const ZapWidget: FC<ZapWidgetProps> = ({ chainId, pool, poolState, title }) => {
: undefined
}, [zapResponse, estGas])

const { sendTransaction, isPending: isWritePending } = useSendTransaction()
const { refetchChain: refetchBalances } = useRefetchBalances()

const onSuccess = useCallback(
(hash: SendTransactionReturnType) => {
if (!chain || !pool) return

setInputAmount('')

const receipt = client.waitForTransactionReceipt({ hash })
receipt.then(() => {
refetchBalances(chain.id)
})

const ts = new Date().getTime()
void createToast({
account: address,
type: 'mint',
chainId: chain.id,
txHash: hash,
promise: receipt,
summary: {
pending: `Zapping into the ${pool.token0.symbol}/${pool.token1.symbol} pair`,
completed: `Successfully zapped into the ${pool.token0.symbol}/${pool.token1.symbol} pair`,
failed: `Something went wrong when zapping into the ${pool.token0.symbol}/${pool.token1.symbol} pair`,
},
timestamp: ts,
groupTimestamp: ts,
})
},
[refetchBalances, client, chain, address, pool],
)

const { sendTransaction, isPending: isWritePending } = useSendTransaction({
mutation: { onSuccess },
})

return (
<>
Expand All @@ -280,44 +357,42 @@ const ZapWidget: FC<ZapWidgetProps> = ({ chainId, pool, poolState, title }) => {
loading={poolState === SushiSwapV2PoolState.LOADING}
allowNative={isWNativeSupported(chainId)}
/>
<CheckerProvider>
<Checker.Connect fullWidth>
<Checker.Network fullWidth chainId={chainId}>
<Checker.Amounts
<Checker.Connect fullWidth>
<Checker.Network fullWidth chainId={chainId}>
<Checker.Amounts
fullWidth
chainId={chainId}
amount={parsedInputAmount}
>
<Checker.ApproveERC20
id="approve-token"
className="whitespace-nowrap"
fullWidth
chainId={chainId}
amount={parsedInputAmount}
contract={zapResponse?.tx.to}
>
<Checker.ApproveERC20
id="approve-token"
className="whitespace-nowrap"
fullWidth
amount={parsedInputAmount}
contract={zapResponse?.tx.to}
>
<Checker.Success tag={APPROVE_TAG_ZAP_LEGACY}>
<Button
size="xl"
fullWidth
testId="zap-liquidity"
onClick={() => preparedTx && sendTransaction(preparedTx)}
loading={!preparedTx || isWritePending}
disabled={isZapError || isEstGasError}
>
{isZapError || isEstGasError ? (
'Shoot! Something went wrong :('
) : isWritePending ? (
<Dots>{title}</Dots>
) : (
title
)}
</Button>
</Checker.Success>
</Checker.ApproveERC20>
</Checker.Amounts>
</Checker.Network>
</Checker.Connect>
</CheckerProvider>
<Checker.Success tag={APPROVE_TAG_ZAP_LEGACY}>
<Button
size="xl"
fullWidth
testId="zap-liquidity"
onClick={() => preparedTx && sendTransaction(preparedTx)}
loading={!preparedTx || isWritePending}
disabled={isZapError || isEstGasError}
>
{isZapError || isEstGasError ? (
'Shoot! Something went wrong :('
) : isWritePending ? (
<Dots>Confirm Transaction</Dots>
) : (
title
)}
</Button>
</Checker.Success>
</Checker.ApproveERC20>
</Checker.Amounts>
</Checker.Network>
</Checker.Connect>
<ZapInfoCard
zapResponse={zapResponse}
inputCurrency={inputCurrency}
Expand Down
39 changes: 21 additions & 18 deletions apps/web/src/app/(networks)/(evm)/api/zap/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,26 @@ const schema = z.object({
routingStrategy: z
.enum(['ensowallet', 'router', 'delegate'])
.default('router'),
// toEoa: z.boolean(), // DEPRECATED
receiver: z.optional(
z.string().transform((receiver) => getAddress(receiver)),
),
spender: z.optional(z.string().transform((spender) => getAddress(spender))),
receiver: z
.string()
.transform((receiver) => getAddress(receiver))
.optional(),
spender: z
.string()
.transform((spender) => getAddress(spender))
.optional(),
amountIn: z.union([z.string(), z.array(z.string())]),
amountOut: z.optional(z.union([z.string(), z.array(z.string())])),
minAmountOut: z.optional(z.union([z.string(), z.array(z.string())])),
slippage: z.optional(z.string()),
fee: z.optional(z.union([z.string(), z.array(z.string())])),
feeReceiver: z.optional(z.string()),
disableAggregators: z.optional(z.string()),
ignoreAggregators: z.optional(z.string()),
ignoreStandards: z.optional(z.string()),
tokenIn: z.optional(z.union([z.string(), z.array(z.string())])),
tokenOut: z.optional(z.union([z.string(), z.array(z.string())])),
quote: z.optional(z.boolean()),
amountOut: z.union([z.string(), z.array(z.string())]).optional(),
minAmountOut: z.union([z.string(), z.array(z.string())]).optional(),
slippage: z.string().optional(), // BIPS
fee: z.union([z.string(), z.array(z.string())]).optional(), // BIPS
feeReceiver: z.string().optional(),
disableAggregators: z.string().optional(),
ignoreAggregators: z.string().optional(),
ignoreStandards: z.string().optional(),
tokenIn: z.union([z.string(), z.array(z.string())]).optional(),
tokenOut: z.union([z.string(), z.array(z.string())]).optional(),
quote: z.boolean().optional(),
})

export const revalidate = 600
Expand All @@ -41,7 +44,7 @@ export async function GET(request: NextRequest) {
const { quote, ...parsedParams } = schema.parse(params)

const queryParams = new URLSearchParams(
Object.entries(params).reduce(
Object.entries(parsedParams).reduce(
(accum: [string, string][], [key, value]) => {
if (value !== undefined && value !== null) {
if (Array.isArray(value)) {
Expand All @@ -68,7 +71,7 @@ export async function GET(request: NextRequest) {
},
)

return new Response(response.body, {
return new Response(await response.text(), {
status: response.status,
headers: {
'Cache-Control': 'max-age=60, stale-while-revalidate=600',
Expand Down
32 changes: 21 additions & 11 deletions apps/web/src/lib/hooks/useZap.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { UseQueryOptions, useQuery } from '@tanstack/react-query'
import { isZapSupportedChainId } from 'src/config'
import { ChainId } from 'sushi/chain'
import { TOKEN_CHOMPER_ADDRESS, isTokenChomperChainId } from 'sushi/config'
import { Percent } from 'sushi/math'
import { Address, Hex } from 'viem'
import { z } from 'zod'

Expand Down Expand Up @@ -45,20 +47,12 @@ export type ZapResponse = z.infer<typeof zapResponseSchema>
type UseZapParams = {
chainId: ChainId
fromAddress?: Address
routingStrategy?: string
receiver?: Address
spender?: Address
amountIn: string | string[]
amountOut?: string | string[]
minAmountOut?: string | string[]
slippage?: string
fee?: string | string[]
feeReceiver?: string
disableRFQs?: boolean
ignoreAggregators?: string | string[]
ignoreStandards?: string | string[]
tokenIn: Address | Address[]
tokenOut?: Address | Address[]
slippage?: Percent
enableFee?: boolean
query?: Omit<UseQueryOptions<ZapResponse>, 'queryKey' | 'queryFn'>
}

Expand All @@ -68,7 +62,9 @@ export const useZap = ({ query, ...params }: UseZapParams) => {
queryFn: async () => {
const url = new URL('/api/zap', window.location.origin)

Object.entries(params).forEach(([key, value]) => {
const { enableFee = true, slippage, ..._params } = params

Object.entries(_params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
if (Array.isArray(value)) {
value.forEach((val) => url.searchParams.append(key, val))
Expand All @@ -78,6 +74,20 @@ export const useZap = ({ query, ...params }: UseZapParams) => {
}
})

if (slippage) {
url.searchParams.set('slippage', slippage.multiply(100n).toFixed(0))
}

if (enableFee) {
url.searchParams.set('fee', '25') // 0.25%
url.searchParams.set(
'feeReceiver',
isTokenChomperChainId(params.chainId)
? TOKEN_CHOMPER_ADDRESS[params.chainId]
: '0xFF64C2d5e23e9c48e8b42a23dc70055EEC9ea098',
)
}

const response = await fetch(url.toString(), {
method: 'GET',
headers: {
Expand Down
Loading

0 comments on commit ae128e6

Please sign in to comment.