diff --git a/src/models/BzzToken.ts b/src/models/BzzToken.ts index 6949c51b..2249480b 100644 --- a/src/models/BzzToken.ts +++ b/src/models/BzzToken.ts @@ -1,8 +1,14 @@ import { BigNumber } from 'bignumber.js' import { Token } from './Token' +export const BZZ_DECIMAL_PLACES = 16 + export class BzzToken extends Token { - constructor(amount: BigNumber | string | bigint) { - super(amount, 16) + constructor(value: BigNumber | string | bigint) { + super(value, BZZ_DECIMAL_PLACES) + } + + static fromDecimal(value: BigNumber | string | bigint): BzzToken { + return Token.fromDecimal(value, BZZ_DECIMAL_PLACES) } } diff --git a/src/models/DaiToken.ts b/src/models/DaiToken.ts index e0b8a193..65fb7854 100644 --- a/src/models/DaiToken.ts +++ b/src/models/DaiToken.ts @@ -1,8 +1,14 @@ import { BigNumber } from 'bignumber.js' import { Token } from './Token' +const DAI_DECIMAL_PLACES = 18 + export class DaiToken extends Token { - constructor(amount: BigNumber | string | bigint) { - super(amount, 18) + constructor(value: BigNumber | string | bigint) { + super(value, DAI_DECIMAL_PLACES) + } + + static fromDecimal(value: BigNumber | string | bigint): DaiToken { + return Token.fromDecimal(value, DAI_DECIMAL_PLACES) } } diff --git a/src/models/Token.ts b/src/models/Token.ts index 10514996..7fb7742f 100644 --- a/src/models/Token.ts +++ b/src/models/Token.ts @@ -87,4 +87,11 @@ export class Token { this.decimals, ) } + + plusBaseUnits(amount: string): Token { + return new Token( + this.toBigNumber.plus(new BigNumber(amount).multipliedBy(new BigNumber(10).pow(this.decimals))), + this.decimals, + ) + } } diff --git a/src/pages/top-up/Swap.tsx b/src/pages/top-up/Swap.tsx index f79e4ea3..80c0907d 100644 --- a/src/pages/top-up/Swap.tsx +++ b/src/pages/top-up/Swap.tsx @@ -1,6 +1,5 @@ import { BeeModes } from '@ethersphere/bee-js' import { Box, Typography } from '@material-ui/core' -import BigNumber from 'bignumber.js' import { useSnackbar } from 'notistack' import { ReactElement, useContext, useEffect, useState } from 'react' import { useNavigate } from 'react-router' @@ -14,11 +13,11 @@ import { Loading } from '../../components/Loading' import { SwarmButton } from '../../components/SwarmButton' import { SwarmDivider } from '../../components/SwarmDivider' import { SwarmTextInput } from '../../components/SwarmTextInput' -import { BzzToken } from '../../models/BzzToken' +import { BzzToken, BZZ_DECIMAL_PLACES } from '../../models/BzzToken' import { DaiToken } from '../../models/DaiToken' -import { Context as BalanceProvider } from '../../providers/WalletBalance' import { Context as BeeContext } from '../../providers/Bee' import { Context as SettingsContext } from '../../providers/Settings' +import { Context as BalanceProvider } from '../../providers/WalletBalance' import { ROUTES } from '../../routes' import { sleepMs } from '../../utils' import { getBzzPriceAsDai, performSwap, restartBeeNode, upgradeToLightNode } from '../../utils/desktop' @@ -31,19 +30,15 @@ interface Props { header: string } -function isPositiveDecimal(value: string): boolean { - try { - return new BigNumber(value).isPositive() - } catch { - return false - } -} - export function Swap({ header }: Props): ReactElement { const [loading, setLoading] = useState(false) const [hasSwapped, setSwapped] = useState(false) const [userInputSwap, setUserInputSwap] = useState(null) - const [price, setPrice] = useState(DaiToken.fromDecimal('0.6', 18)) + const [price, setPrice] = useState(DaiToken.fromDecimal('0.6')) + const [error, setError] = useState(null) + const [daiToSwap, setDaiToSwap] = useState(null) + const [bzzAfterSwap, setBzzAfterSwap] = useState(null) + const [daiAfterSwap, setDaiAfterSwap] = useState(null) const { rpcProviderUrl, isDesktop, desktopUrl } = useContext(SettingsContext) const { nodeAddresses, nodeInfo } = useContext(BeeContext) @@ -52,28 +47,69 @@ export function Swap({ header }: Props): ReactElement { const navigate = useNavigate() const { enqueueSnackbar } = useSnackbar() + // Fetch current price of BZZ useEffect(() => { // eslint-disable-next-line no-console getBzzPriceAsDai(desktopUrl).then(setPrice).catch(console.error) }, [desktopUrl]) - if (!balance || !nodeAddresses) { - return - } + // Set the initial xDAI to swap + useEffect(() => { + if (!balance) { + return + } + + const minimumOptimalValue = DaiToken.fromDecimal('1').plusBaseUnits(MINIMUM_XDAI).toDecimal - const optimalSwap = balance.dai.minusBaseUnits('1') - const lowAmountSwap = new DaiToken(balance.dai.toBigNumber.dividedToIntegerBy(2)) + if (balance.dai.toDecimal.isGreaterThanOrEqualTo(minimumOptimalValue)) { + // Balance has at least 1 + MINIMUM_XDAI xDai + setDaiToSwap(balance.dai.minusBaseUnits('1')) + } else { + // Balance is low, halve the amount + setDaiToSwap(new DaiToken(balance.dai.toBigNumber.dividedToIntegerBy(2))) + } + }, [balance]) - let daiToSwap: DaiToken + // Set the xDAI to swap based on user input + useEffect(() => { + setError(null) + try { + if (userInputSwap) { + const dai = DaiToken.fromDecimal(userInputSwap) + setDaiToSwap(dai) + + if (dai.toDecimal.lte(0)) { + setError('xDAI to swap must be a positive number') + } + } + } catch { + setError('Cannot parse xDAI amount') + } + }, [userInputSwap]) - if (userInputSwap && isPositiveDecimal(userInputSwap)) { - daiToSwap = DaiToken.fromDecimal(userInputSwap, 18) - } else { - daiToSwap = lowAmountSwap.toBigNumber.gt(optimalSwap.toBigNumber) ? lowAmountSwap : optimalSwap - } + // Calculate the amount of tokens after swap + useEffect(() => { + if (!balance || !daiToSwap || error) { + return + } + const daiAfterSwap = new DaiToken(balance.dai.toBigNumber.minus(daiToSwap.toBigNumber)) + setDaiAfterSwap(daiAfterSwap) + const tokensConverted = BzzToken.fromDecimal( + daiToSwap.toBigNumber.dividedBy(price.toBigNumber).decimalPlaces(BZZ_DECIMAL_PLACES), + ) + const bzzAfterSwap = new BzzToken(tokensConverted.toBigNumber.plus(balance.bzz.toBigNumber)) + setBzzAfterSwap(bzzAfterSwap) + + if (daiAfterSwap.toDecimal.lt(MINIMUM_XDAI)) { + setError(`Must keep at least ${MINIMUM_XDAI} xDAI after swap!`) + } else if (bzzAfterSwap.toDecimal.lt(MINIMUM_XBZZ)) { + setError(`Must have at least ${MINIMUM_XBZZ} xBZZ after swap!`) + } + }, [error, balance, daiToSwap, price]) - const daiAfterSwap = new DaiToken(balance.dai.toBigNumber.minus(daiToSwap.toBigNumber)) - const bzzAfterSwap = new BzzToken(daiToSwap.toBigNumber.dividedBy(100).dividedToIntegerBy(price.toDecimal)) + if (!balance || !nodeAddresses || !daiToSwap || !bzzAfterSwap || !daiAfterSwap) { + return + } const canUpgradeToLightNode = isDesktop && nodeInfo?.beeMode === BeeModes.ULTRA_LIGHT @@ -92,7 +128,7 @@ export function Swap({ header }: Props): ReactElement { } async function onSwap() { - if (hasSwapped) { + if (hasSwapped || !daiToSwap) { return } setLoading(true) @@ -100,7 +136,10 @@ export function Swap({ header }: Props): ReactElement { try { await performSwap(desktopUrl, daiToSwap.toString) - enqueueSnackbar('Successfully swapped', { variant: 'success' }) + const message = canUpgradeToLightNode + ? 'Successfully swapped. Beginning light node upgrade...' + : 'Successfully swapped. Balances will refresh soon. You may now leave the page.' + enqueueSnackbar(message, { variant: 'success' }) if (canUpgradeToLightNode) await restart() } catch (error) { @@ -136,18 +175,13 @@ export function Swap({ header }: Props): ReactElement { setUserInputSwap(event.target.value)} /> - {daiAfterSwap.toDecimal.lt(MINIMUM_XDAI) ? ( - Must keep at least {MINIMUM_XDAI} xDAI after swap! - ) : null} - {bzzAfterSwap.toDecimal.lt(MINIMUM_XBZZ) ? ( - Must have at least {MINIMUM_XBZZ} xBZZ after swap! - ) : null} + {error && {error}} @@ -171,9 +205,7 @@ export function Swap({ header }: Props): ReactElement { {canUpgradeToLightNode ? 'Swap Now and Upgrade' : 'Swap Now'} diff --git a/src/utils/desktop.ts b/src/utils/desktop.ts index 6d9ee96d..76102c0d 100644 --- a/src/utils/desktop.ts +++ b/src/utils/desktop.ts @@ -1,13 +1,13 @@ import axios from 'axios' +import { BEE_DESKTOP_LATEST_RELEASE_PAGE_API } from '../constants' import { DaiToken } from '../models/DaiToken' import { Token } from '../models/Token' import { postJson } from './net' -import { BEE_DESKTOP_LATEST_RELEASE_PAGE_API } from '../constants' export async function getBzzPriceAsDai(desktopUrl: string): Promise { const response = await axios.get(`${desktopUrl}/price`) - return DaiToken.fromDecimal(response.data, 18) + return DaiToken.fromDecimal(response.data) } export async function upgradeToLightNode(desktopUrl: string, rpcProvider: string): Promise {