From c2b8fa895ee46f91433707daafa60b336692cb46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luiz=20Est=C3=A1cio=20=7C=20stacio=2Eeth?= Date: Tue, 24 May 2022 17:33:58 -0300 Subject: [PATCH] feat: return reserves and fee on price preview (#190) * feat: return reserve and fee on preview prices * chore: prevent vercel to comment * feat: add swap preview info * feat: fix swap card to top * refact: split code and improve behaviors * feat: fix isssues and improve loading * chore: prettify * chore: update contract id * fix: pass contract * config: update fuel-core version * refact: refactor based on PR comments * fix: fix exchange contract tests --- .github/workflows/gh-pages.yml | 2 +- contracts/exchange_contract/Forc.lock | 8 +- contracts/exchange_contract/src/main.sw | 16 +-- contracts/exchange_contract/tests/harness.rs | 16 +++ docker/fuel-core/Dockerfile | 2 +- packages/app/src/components/PreviewTable.tsx | 34 ++++++ packages/app/src/config.ts | 1 + packages/app/src/hooks/usePoolInfo.ts | 7 ++ packages/app/src/hooks/useSlippage.ts | 8 ++ packages/app/src/lib/constants.ts | 5 +- packages/app/src/lib/utils.ts | 5 + .../app/src/pages/SwapPage/PricePerToken.tsx | 23 ++-- .../app/src/pages/SwapPage/SwapComponent.tsx | 36 +++---- .../app/src/pages/SwapPage/SwapPreview.tsx | 80 ++++++++++++++ packages/app/src/pages/SwapPage/helpers.ts | 54 ++++++++++ packages/app/src/pages/SwapPage/index.tsx | 101 +++++++++++++----- packages/app/src/pages/SwapPage/jotai.ts | 22 +++- packages/app/src/pages/SwapPage/queries.ts | 24 +++-- packages/app/src/pages/SwapPage/types.ts | 23 +++- vercel.json | 5 + 20 files changed, 390 insertions(+), 82 deletions(-) create mode 100644 packages/app/src/components/PreviewTable.tsx create mode 100644 packages/app/src/hooks/usePoolInfo.ts create mode 100644 packages/app/src/hooks/useSlippage.ts create mode 100644 packages/app/src/pages/SwapPage/SwapPreview.tsx create mode 100644 packages/app/src/pages/SwapPage/helpers.ts create mode 100644 vercel.json diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index bb67bca0..f2e14c02 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -46,7 +46,7 @@ jobs: CI: false PUBLIC_URL: "/${{ github.event.repository.name }}" VITE_FUEL_PROVIDER_URL: "https://node.swayswap.io/graphql" - VITE_CONTRACT_ID: "0xad3134370511cecb84064622a87b6f7472e7d6423b8e9ea13753bed4c58fc1c2" + VITE_CONTRACT_ID: "0xac01cffc1fe91976228834078e888efcd185d6792bd2138b09afbdf833599ef6" VITE_TOKEN_ID: "0xb72c566e5a9f69c98298a04d70a38cb32baca4d9b280da8590e0314fb00c59e0" run: | pnpm build diff --git a/contracts/exchange_contract/Forc.lock b/contracts/exchange_contract/Forc.lock index d0cc1a32..d49bda35 100644 --- a/contracts/exchange_contract/Forc.lock +++ b/contracts/exchange_contract/Forc.lock @@ -4,21 +4,21 @@ dependencies = [] [[package]] name = 'exchange_abi' -dependencies = ['std git+https://github.com/fuellabs/sway?tag=v0.13.0#6eef7ab750cd3282f08b6014960cbc02afae717a'] +dependencies = ['std git+https://github.com/fuellabs/sway?tag=v0.13.1#d73d9d2b4b547e2035ddfe085d439de56ccee1f0'] [[package]] name = 'exchange_contract' dependencies = [ 'exchange_abi', - 'std git+https://github.com/fuellabs/sway?tag=v0.13.0#6eef7ab750cd3282f08b6014960cbc02afae717a', + 'std git+https://github.com/fuellabs/sway?tag=v0.13.1#d73d9d2b4b547e2035ddfe085d439de56ccee1f0', 'swayswap_helpers', ] [[package]] name = 'std' -source = 'git+https://github.com/fuellabs/sway?tag=v0.13.0#6eef7ab750cd3282f08b6014960cbc02afae717a' +source = 'git+https://github.com/fuellabs/sway?tag=v0.13.1#d73d9d2b4b547e2035ddfe085d439de56ccee1f0' dependencies = ['core'] [[package]] name = 'swayswap_helpers' -dependencies = ['std git+https://github.com/fuellabs/sway?tag=v0.13.0#6eef7ab750cd3282f08b6014960cbc02afae717a'] +dependencies = ['std git+https://github.com/fuellabs/sway?tag=v0.13.1#d73d9d2b4b547e2035ddfe085d439de56ccee1f0'] diff --git a/contracts/exchange_contract/src/main.sw b/contracts/exchange_contract/src/main.sw index 8f4564c3..fb6b6e7d 100644 --- a/contracts/exchange_contract/src/main.sw +++ b/contracts/exchange_contract/src/main.sw @@ -28,6 +28,8 @@ const TOKEN_ID = 0xb72c566e5a9f69c98298a04d70a38cb32baca4d9b280da8590e0314fb00c5 /// Minimum ETH liquidity to open a pool. const MINIMUM_LIQUIDITY = 1; //A more realistic value would be 1000000000; +// Liquidity miner fee apply to all swaps +const LIQUIDITY_MINER_FEE = 333; //////////////////////////////////////// // Storage declarations @@ -69,7 +71,7 @@ fn remove_reserve(token_id: b256, amount: u64) { // Calculate 0.3% fee fn calculate_amount_with_fee(amount: u64) -> u64 { - let fee: u64 = (amount / 333); + let fee: u64 = (amount / LIQUIDITY_MINER_FEE); amount - fee } @@ -336,7 +338,7 @@ impl Exchange for Contract { let eth_reserve = get_current_reserve(ETH_ID); let token_reserve = get_current_reserve(TOKEN_ID); let mut sold = 0; - let mut has_liquidity = false; + let mut has_liquidity = true; if (msg_asset_id().into() == ETH_ID) { sold = get_input_price(amount, eth_reserve, token_reserve); has_liquidity = sold < token_reserve; @@ -346,7 +348,7 @@ impl Exchange for Contract { } PreviewInfo { amount: sold, - has_liquidity: has_liquidity + has_liquidity: has_liquidity, } } @@ -354,18 +356,18 @@ impl Exchange for Contract { let eth_reserve = get_current_reserve(ETH_ID); let token_reserve = get_current_reserve(TOKEN_ID); let mut sold = 0; - let mut has_liquidity = false; + let mut has_liquidity = true; if (msg_asset_id().into() == ETH_ID) { sold = get_output_price(amount, eth_reserve, token_reserve); - has_liquidity = sold < token_reserve; + has_liquidity = sold < eth_reserve; } else { sold = get_output_price(amount, token_reserve, eth_reserve); - has_liquidity = sold < eth_reserve; + has_liquidity = sold < token_reserve; } PreviewInfo { amount: sold, - has_liquidity: has_liquidity + has_liquidity: has_liquidity, } } } diff --git a/contracts/exchange_contract/tests/harness.rs b/contracts/exchange_contract/tests/harness.rs index 432db98e..499d0178 100644 --- a/contracts/exchange_contract/tests/harness.rs +++ b/contracts/exchange_contract/tests/harness.rs @@ -233,6 +233,22 @@ async fn exchange_contract() { // Inspect the wallet for alt tokens to be 100 assert_eq!(total_amount, 100); + // Return reserve amounts and fee + let result = exchange_instance + .get_swap_with_minimum(10) + .call() + .await + .unwrap(); + assert_eq!(result.value.amount, 9); + assert!(result.value.has_liquidity); + let result = exchange_instance + .get_swap_with_maximum(10) + .call() + .await + .unwrap(); + assert_eq!(result.value.amount, 12); + assert!(result.value.has_liquidity); + //////////////////// // SWAP WITH MINIMUM //////////////////// diff --git a/docker/fuel-core/Dockerfile b/docker/fuel-core/Dockerfile index d64f53a0..7afc9477 100644 --- a/docker/fuel-core/Dockerfile +++ b/docker/fuel-core/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/fuellabs/fuel-core:v0.6.4 +FROM ghcr.io/fuellabs/fuel-core:v0.7.1 ARG IP=0.0.0.0 ARG PORT=4000 diff --git a/packages/app/src/components/PreviewTable.tsx b/packages/app/src/components/PreviewTable.tsx new file mode 100644 index 00000000..9a7cd4d8 --- /dev/null +++ b/packages/app/src/components/PreviewTable.tsx @@ -0,0 +1,34 @@ +import classNames from "classnames"; +import type { ReactNode } from "react"; + +type TableItemProps = { + title: ReactNode; + value: ReactNode; + className?: string; +}; + +export const PreviewItem = ({ title, value, className }: TableItemProps) => ( +
+
{title}
+
{value}
+
+); + +type PreviewTableProps = { + title: ReactNode; + children: ReactNode; + className?: string; +}; + +export const PreviewTable = ({ + title, + children, + className, +}: PreviewTableProps) => ( +
+
{title}
+
+ {children} +
+
+); diff --git a/packages/app/src/config.ts b/packages/app/src/config.ts index 190f6e08..c486e99a 100644 --- a/packages/app/src/config.ts +++ b/packages/app/src/config.ts @@ -15,6 +15,7 @@ export const ONE_ASSET = parseUnits('1', DECIMAL_UNITS).toBigInt(); export const RECAPTCHA_SITE_KEY = import.meta.env.VITE_RECAPTCHA_SITE_KEY!; export const ENABLE_FAUCET_API = import.meta.env.VITE_ENABLE_FAUCET_API === 'true'; export const SLIPPAGE_TOLERANCE = 0.005; +export const NETWORK_FEE = 1; // Max value supported // eslint-disable-next-line @typescript-eslint/no-loss-of-precision diff --git a/packages/app/src/hooks/usePoolInfo.ts b/packages/app/src/hooks/usePoolInfo.ts new file mode 100644 index 00000000..6272ed9e --- /dev/null +++ b/packages/app/src/hooks/usePoolInfo.ts @@ -0,0 +1,7 @@ +import { useQuery } from 'react-query'; + +import type { ExchangeContractAbi } from '~/types/contracts'; + +export function usePoolInfo(contract: ExchangeContractAbi) { + return useQuery('PoolPage-poolInfo', () => contract.callStatic.get_info()); +} diff --git a/packages/app/src/hooks/useSlippage.ts b/packages/app/src/hooks/useSlippage.ts new file mode 100644 index 00000000..461e5cae --- /dev/null +++ b/packages/app/src/hooks/useSlippage.ts @@ -0,0 +1,8 @@ +import { SLIPPAGE_TOLERANCE } from '~/config'; + +export function useSlippage() { + return { + value: SLIPPAGE_TOLERANCE, + formatted: `${SLIPPAGE_TOLERANCE * 100}%`, + }; +} diff --git a/packages/app/src/lib/constants.ts b/packages/app/src/lib/constants.ts index d4bbe414..2835ed8a 100644 --- a/packages/app/src/lib/constants.ts +++ b/packages/app/src/lib/constants.ts @@ -1,4 +1,7 @@ +import { toBigInt } from 'fuels'; + export const contractABI = {}; export const contractAddress = '0xF93c18172eAba6a9F145B3FB16d2bBeA2e096477'; -export const LocalStorageKey = 'swayswap'; +export const LocalStorageKey = 'swayswap-v2'; export const CoinETH = '0x0000000000000000000000000000000000000000000000000000000000000000'; +export const ZERO = toBigInt(0); diff --git a/packages/app/src/lib/utils.ts b/packages/app/src/lib/utils.ts index d4439522..7d20ed3d 100644 --- a/packages/app/src/lib/utils.ts +++ b/packages/app/src/lib/utils.ts @@ -1,3 +1,4 @@ +import { BigNumber } from 'ethers'; import type { BigNumberish } from 'fuels'; import urljoin from 'url-join'; @@ -22,3 +23,7 @@ export function omit(list: string[], props: T) { return { ...obj, [key]: value }; }, {} as T) as T; } + +export function divideBigInt(from: BigNumberish, to: BigNumberish) { + return BigNumber.from(from).toNumber() / BigNumber.from(to).toNumber(); +} diff --git a/packages/app/src/pages/SwapPage/PricePerToken.tsx b/packages/app/src/pages/SwapPage/PricePerToken.tsx index 26519561..4fb34eeb 100644 --- a/packages/app/src/pages/SwapPage/PricePerToken.tsx +++ b/packages/app/src/pages/SwapPage/PricePerToken.tsx @@ -1,14 +1,13 @@ -import { BigNumber } from "ethers"; -import { formatUnits } from "ethers/lib/utils"; -import { useAtomValue } from "jotai"; +import { toNumber } from "fuels"; import { useState } from "react"; import { AiOutlineSwap } from "react-icons/ai"; -import { swapIsTypingAtom } from "./jotai"; +import { useValueIsTyping } from "./jotai"; import { ActiveInput } from "./types"; import { Button } from "~/components/Button"; -import { DECIMAL_UNITS, ONE_ASSET } from "~/config"; +import { ONE_ASSET } from "~/config"; +import { divideBigInt } from "~/lib/utils"; const style = { wrapper: `flex items-center gap-3 my-4 px-2 text-sm text-gray-400`, @@ -22,10 +21,10 @@ function getPricePerToken( if (!toAmount || !fromAmount) return ""; const ratio = direction === ActiveInput.from - ? BigNumber.from(fromAmount || 0).div(toAmount || 0) - : BigNumber.from(toAmount || 0).div(fromAmount || 0); - const price = ratio.mul(ONE_ASSET); - return formatUnits(price, DECIMAL_UNITS); + ? divideBigInt(fromAmount, toAmount) + : divideBigInt(toAmount, fromAmount); + const price = ratio * toNumber(ONE_ASSET); + return (price / toNumber(ONE_ASSET)).toFixed(6); } type PricePerTokenProps = { @@ -33,6 +32,7 @@ type PricePerTokenProps = { fromAmount?: bigint | null; toCoin?: string; toAmount?: bigint | null; + isLoading?: boolean; }; export function PricePerToken({ @@ -40,9 +40,10 @@ export function PricePerToken({ fromAmount, toCoin, toAmount, + isLoading, }: PricePerTokenProps) { const [direction, setDirection] = useState(ActiveInput.to); - const isTyping = useAtomValue(swapIsTypingAtom); + const isTyping = useValueIsTyping(); const pricePerToken = getPricePerToken(direction, fromAmount, toAmount); const from = direction === ActiveInput.from ? toCoin : fromCoin; @@ -54,7 +55,7 @@ export function PricePerToken({ ); } - if (isTyping) return null; + if (isTyping || isLoading) return null; if (!fromAmount || !toAmount) return null; return ( diff --git a/packages/app/src/pages/SwapPage/SwapComponent.tsx b/packages/app/src/pages/SwapPage/SwapComponent.tsx index 9f933917..7855ec83 100644 --- a/packages/app/src/pages/SwapPage/SwapComponent.tsx +++ b/packages/app/src/pages/SwapPage/SwapComponent.tsx @@ -1,13 +1,13 @@ -import { useAtom, useAtomValue, useSetAtom } from "jotai"; +import { toBigInt } from "fuels"; +import { useAtom, useAtomValue } from "jotai"; import { startTransition, useEffect } from "react"; -import { PricePerToken } from "./PricePerToken"; import { swapActiveInputAtom, swapAmountAtom, swapCoinsAtom, - swapIsTypingAtom, swapHasSwappedAtom, + useSetIsTyping, } from "./jotai"; import type { SwapState } from "./types"; import { ActiveInput } from "./types"; @@ -15,6 +15,7 @@ import { ActiveInput } from "./types"; import { CoinInput, useCoinInput } from "~/components/CoinInput"; import { CoinSelector } from "~/components/CoinSelector"; import { InvertButton } from "~/components/InvertButton"; +import { NETWORK_FEE } from "~/config"; import type { Coin } from "~/types"; const style = { @@ -30,15 +31,16 @@ type SwapComponentProps = { export function SwapComponent({ onChange, isLoading, - previewAmount: previewValue, + previewAmount, }: SwapComponentProps) { const [initialAmount, setInitialAmount] = useAtom(swapAmountAtom); const [activeInput, setActiveInput] = useAtom(swapActiveInputAtom); const [[coinFrom, coinTo], setCoins] = useAtom(swapCoinsAtom); - const setTyping = useSetAtom(swapIsTypingAtom); const hasSwapped = useAtomValue(swapHasSwappedAtom); + const setTyping = useSetIsTyping(); const handleInvertCoins = () => { + setTyping(true); if (activeInput === ActiveInput.to) { const from = fromInput.amount; startTransition(() => { @@ -60,6 +62,7 @@ export function SwapComponent({ const fromInput = useCoinInput({ coin: coinFrom, disableWhenEth: true, + gasFee: toBigInt(NETWORK_FEE), onChangeCoin: (coin: Coin) => { setCoins([coin, coinTo]); }, @@ -92,7 +95,6 @@ export function SwapComponent({ useEffect(() => { const currentInput = activeInput === ActiveInput.from ? fromInput : toInput; const amount = currentInput.amount; - const coin = activeInput === ActiveInput.from ? coinFrom : coinTo; // This is used to reset preview amount when set first input value for null if (activeInput === ActiveInput.from && amount === null) { @@ -105,13 +107,13 @@ export function SwapComponent({ // Set value to hydrate setInitialAmount(amount); - if (coin && coinFrom && coinTo) { + if (coinFrom && coinTo) { // Call on onChange onChange?.({ amount, - coin, - from: coinFrom?.assetId, - to: coinTo?.assetId, + amountFrom: fromInput.amount, + coinFrom, + coinTo, direction: activeInput, hasBalance: fromInput.hasEnoughBalance, }); @@ -119,14 +121,12 @@ export function SwapComponent({ }, [fromInput.amount, toInput.amount, coinFrom, coinTo]); useEffect(() => { - if (previewValue == null) return; if (activeInput === ActiveInput.from) { - toInput.setAmount(previewValue); + toInput.setAmount(previewAmount || null); } else { - fromInput.setAmount(previewValue); + fromInput.setAmount(previewAmount || null); } - setTyping(false); - }, [previewValue]); + }, [previewAmount]); useEffect(() => { if (hasSwapped) { @@ -156,12 +156,6 @@ export function SwapComponent({ rightElement={} /> - ); } diff --git a/packages/app/src/pages/SwapPage/SwapPreview.tsx b/packages/app/src/pages/SwapPage/SwapPreview.tsx new file mode 100644 index 00000000..f505e2cb --- /dev/null +++ b/packages/app/src/pages/SwapPage/SwapPreview.tsx @@ -0,0 +1,80 @@ +import { formatUnits } from "ethers/lib/utils"; +import { BsArrowDown } from "react-icons/bs"; + +import { calculatePriceImpact, calculatePriceWithSlippage } from "./helpers"; +import { useValueIsTyping } from "./jotai"; +import type { SwapInfo } from "./types"; +import { ActiveInput } from "./types"; + +import { PreviewItem, PreviewTable } from "~/components/PreviewTable"; +import { DECIMAL_UNITS, NETWORK_FEE } from "~/config"; +import { useSlippage } from "~/hooks/useSlippage"; +import { ZERO } from "~/lib/constants"; + +type SwapPreviewProps = { + swapInfo: SwapInfo; + isLoading: boolean; +}; + +export function SwapPreview({ swapInfo, isLoading }: SwapPreviewProps) { + const { amount, previewAmount, direction, coinFrom, coinTo } = swapInfo; + const isTyping = useValueIsTyping(); + const slippage = useSlippage(); + + if ( + !coinFrom || + !coinTo || + !previewAmount || + !direction || + !amount || + isLoading || + isTyping + ) { + return null; + } + // Expected amount of tokens to be received + const outputAmount = formatUnits( + direction === ActiveInput.from ? previewAmount : amount || ZERO, + DECIMAL_UNITS + ); + + return ( +
+
+ +
+ + + + + + +
+ ); +} diff --git a/packages/app/src/pages/SwapPage/helpers.ts b/packages/app/src/pages/SwapPage/helpers.ts new file mode 100644 index 00000000..0c196d07 --- /dev/null +++ b/packages/app/src/pages/SwapPage/helpers.ts @@ -0,0 +1,54 @@ +import { toNumber } from 'fuels'; + +import type { SwapInfo } from './types'; +import { ActiveInput } from './types'; + +import { CoinETH } from '~/lib/constants'; + +export function getPriceImpact( + outputAmount: bigint, + inputAmount: bigint, + reserveInput: bigint, + reserveOutput: bigint +) { + const exchangeRateAfter = toNumber(inputAmount) / toNumber(outputAmount); + const exchangeRateBefore = toNumber(reserveInput) / toNumber(reserveOutput); + return ((exchangeRateAfter / exchangeRateBefore - 1) * 100).toFixed(2); +} + +export const calculatePriceImpact = ({ + direction, + amount, + coinFrom, + previewAmount, + token_reserve, + eth_reserve, +}: SwapInfo) => { + // If any value is 0 return 0 + if (!previewAmount || !amount || !token_reserve || !eth_reserve) return '0'; + + if (direction === ActiveInput.from) { + if (coinFrom?.assetId !== CoinETH) { + return getPriceImpact(previewAmount, amount, token_reserve, eth_reserve); + } + return getPriceImpact(previewAmount, amount, eth_reserve, token_reserve); + } + if (coinFrom?.assetId !== CoinETH) { + return getPriceImpact(amount, previewAmount, token_reserve, eth_reserve); + } + return getPriceImpact(amount, previewAmount, eth_reserve, token_reserve); +}; + +export const calculatePriceWithSlippage = ( + amount: bigint, + slippage: number, + direction: ActiveInput +) => { + let total = 0; + if (direction === ActiveInput.from) { + total = toNumber(amount) * (1 - slippage); + } else { + total = toNumber(amount) * (1 + slippage); + } + return Math.trunc(total); +}; diff --git a/packages/app/src/pages/SwapPage/index.tsx b/packages/app/src/pages/SwapPage/index.tsx index 48f53900..a2d8324e 100644 --- a/packages/app/src/pages/SwapPage/index.tsx +++ b/packages/app/src/pages/SwapPage/index.tsx @@ -1,35 +1,40 @@ +import type { CoinQuantity } from "fuels"; +import { toNumber } from "fuels"; import { useSetAtom } from "jotai"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import toast from "react-hot-toast"; import { MdSwapCalls } from "react-icons/md"; import { useMutation, useQuery } from "react-query"; +import { PricePerToken } from "./PricePerToken"; import { SwapComponent } from "./SwapComponent"; +import { SwapPreview } from "./SwapPreview"; +import { calculatePriceWithSlippage } from "./helpers"; import { swapHasSwappedAtom } from "./jotai"; import { queryPreviewAmount, swapTokens } from "./queries"; -import type { SwapState } from "./types"; +import type { SwapInfo, SwapState } from "./types"; +import { ActiveInput, ValidationStateEnum } from "./types"; import { Button } from "~/components/Button"; import { Card } from "~/components/Card"; import { useContract } from "~/context/AppContext"; +import { useBalances } from "~/hooks/useBalances"; import useDebounce from "~/hooks/useDebounce"; +import { usePoolInfo } from "~/hooks/usePoolInfo"; +import { useSlippage } from "~/hooks/useSlippage"; +import { ZERO } from "~/lib/constants"; import { isSwayInfinity, sleep } from "~/lib/utils"; import { queryClient } from "~/queryClient"; +import type { PreviewInfo } from "~/types/contracts/ExchangeContractAbi"; type StateParams = { swapState: SwapState | null; previewAmount: bigint | null; hasLiquidity: boolean; + slippage: number; + balances?: CoinQuantity[]; }; -enum ValidationStateEnum { - SelectToken = 0, - EnterAmount = 1, - InsufficientBalance = 2, - InsufficientLiquidity = 3, - Swap = 4, -} - const getValidationText = ( state: ValidationStateEnum, swapState: SwapState | null @@ -40,7 +45,9 @@ const getValidationText = ( case ValidationStateEnum.EnterAmount: return "Enter amount"; case ValidationStateEnum.InsufficientBalance: - return `Insufficient ${swapState?.coin.symbol || ""} balance`; + return `Insufficient ${swapState?.coinFrom.symbol || ""} balance`; + case ValidationStateEnum.InsufficientAmount: + return `Insufficient amount to swap`; case ValidationStateEnum.InsufficientLiquidity: return "Insufficient liquidity"; default: @@ -48,18 +55,36 @@ const getValidationText = ( } }; -const getValidationState = ({ +const hasBalanceWithSlippage = ({ swapState, previewAmount, - hasLiquidity, -}: StateParams): ValidationStateEnum => { - if (!swapState?.to || !swapState?.from) { + slippage, + balances, +}: StateParams) => { + if (swapState!.direction === ActiveInput.to) { + const amountWithSlippage = calculatePriceWithSlippage( + previewAmount || ZERO, + slippage, + swapState!.direction + ); + const currentBalance = toNumber( + balances?.find((coin) => coin.assetId === swapState!.coinFrom.assetId) + ?.amount || ZERO + ); + return amountWithSlippage > currentBalance; + } + return false; +}; + +const getValidationState = (stateParams: StateParams): ValidationStateEnum => { + const { swapState, previewAmount, hasLiquidity } = stateParams; + if (!swapState?.coinFrom || !swapState?.coinTo) { return ValidationStateEnum.SelectToken; } if (!swapState?.amount) { return ValidationStateEnum.EnterAmount; } - if (!swapState.hasBalance) { + if (!swapState.hasBalance || hasBalanceWithSlippage(stateParams)) { return ValidationStateEnum.InsufficientBalance; } if (!hasLiquidity || isSwayInfinity(previewAmount)) @@ -69,10 +94,23 @@ const getValidationState = ({ export default function SwapPage() { const contract = useContract()!; - const [previewAmount, setPreviewAmount] = useState(null); + const [previewInfo, setPreviewInfo] = useState(null); const [swapState, setSwapState] = useState(null); const [hasLiquidity, setHasLiquidity] = useState(true); const debouncedState = useDebounce(swapState); + const { data: poolInfo } = usePoolInfo(contract); + const previewAmount = previewInfo?.amount || ZERO; + const swapInfo = useMemo( + () => ({ + ...poolInfo, + ...previewInfo, + ...swapState, + previewAmount, + }), + [poolInfo, previewInfo, swapState] + ); + const slippage = useSlippage(); + const { data: balances } = useBalances(); const setHasSwapped = useSetAtom(swapHasSwappedAtom); const { isLoading } = useQuery( @@ -80,18 +118,23 @@ export default function SwapPage() { "SwapPage-inactiveAmount", debouncedState?.amount?.toString(), debouncedState?.direction, - debouncedState?.from, - debouncedState?.to, + debouncedState?.coinFrom.assetId, + debouncedState?.coinTo.assetId, ], async () => { if (!debouncedState?.amount) return null; return queryPreviewAmount(contract, debouncedState); }, { - onSuccess: (value) => { - if (value == null) return; - setPreviewAmount(value.amount); - setHasLiquidity(value.has_liquidity); + onSuccess: (preview) => { + if (preview == null) return; + if (isSwayInfinity(preview.amount)) { + setPreviewInfo(null); + setHasLiquidity(false); + } else { + setHasLiquidity(preview.has_liquidity); + setPreviewInfo(preview); + } }, } ); @@ -117,6 +160,8 @@ export default function SwapPage() { const validationState = getValidationState({ swapState, + balances, + slippage: slippage.value, previewAmount, hasLiquidity, }); @@ -125,7 +170,7 @@ export default function SwapPage() { isLoading || validationState !== ValidationStateEnum.Swap; return ( - + Swap @@ -135,6 +180,14 @@ export default function SwapPage() { onChange={handleSwap} isLoading={isLoading} /> + +