Skip to content

Commit

Permalink
fix: always consider user input when performing swap (#572)
Browse files Browse the repository at this point in the history
* fix: always consider user input when performing swap

* refactor: extract decimal places constants

* refactor: extract minimumOptimalValue

* fix: handle bzz precision and tweak message
  • Loading branch information
Cafe137 committed Nov 9, 2022
1 parent a4b8e7c commit ec8fdf0
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 45 deletions.
10 changes: 8 additions & 2 deletions src/models/BzzToken.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
10 changes: 8 additions & 2 deletions src/models/DaiToken.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
7 changes: 7 additions & 0 deletions src/models/Token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}
}
110 changes: 71 additions & 39 deletions src/pages/top-up/Swap.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'
Expand All @@ -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<string | null>(null)
const [price, setPrice] = useState(DaiToken.fromDecimal('0.6', 18))
const [price, setPrice] = useState(DaiToken.fromDecimal('0.6'))
const [error, setError] = useState<string | null>(null)
const [daiToSwap, setDaiToSwap] = useState<DaiToken | null>(null)
const [bzzAfterSwap, setBzzAfterSwap] = useState<BzzToken | null>(null)
const [daiAfterSwap, setDaiAfterSwap] = useState<DaiToken | null>(null)

const { rpcProviderUrl, isDesktop, desktopUrl } = useContext(SettingsContext)
const { nodeAddresses, nodeInfo } = useContext(BeeContext)
Expand All @@ -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 <Loading />
}
// 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 <Loading />
}

const canUpgradeToLightNode = isDesktop && nodeInfo?.beeMode === BeeModes.ULTRA_LIGHT

Expand All @@ -92,15 +128,18 @@ export function Swap({ header }: Props): ReactElement {
}

async function onSwap() {
if (hasSwapped) {
if (hasSwapped || !daiToSwap) {
return
}
setLoading(true)
setSwapped(true)

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) {
Expand Down Expand Up @@ -136,18 +175,13 @@ export function Swap({ header }: Props): ReactElement {
</Box>
<Box mb={4}>
<SwarmTextInput
label="Amount to swap"
defaultValue={`${daiToSwap.toSignificantDigits(4)} xDAI`}
placeholder={`${daiToSwap.toSignificantDigits(4)} xDAI`}
label="xDAI to swap"
defaultValue={daiToSwap.toSignificantDigits(4)}
placeholder={daiToSwap.toSignificantDigits(4)}
name="x"
onChange={event => setUserInputSwap(event.target.value)}
/>
{daiAfterSwap.toDecimal.lt(MINIMUM_XDAI) ? (
<Typography>Must keep at least {MINIMUM_XDAI} xDAI after swap!</Typography>
) : null}
{bzzAfterSwap.toDecimal.lt(MINIMUM_XBZZ) ? (
<Typography>Must have at least {MINIMUM_XBZZ} xBZZ after swap!</Typography>
) : null}
{error && <Typography>{error}</Typography>}
</Box>
<Box mb={4}>
<ArrowDown size={24} color="#aaaaaa" />
Expand All @@ -171,9 +205,7 @@ export function Swap({ header }: Props): ReactElement {
<SwarmButton
iconType={Check}
onClick={onSwap}
disabled={
hasSwapped || loading || daiAfterSwap.toDecimal.lt(MINIMUM_XDAI) || bzzAfterSwap.toDecimal.lt(MINIMUM_XBZZ)
}
disabled={hasSwapped || loading || error !== null}
loading={loading}
>
{canUpgradeToLightNode ? 'Swap Now and Upgrade' : 'Swap Now'}
Expand Down
4 changes: 2 additions & 2 deletions src/utils/desktop.ts
Original file line number Diff line number Diff line change
@@ -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<Token> {
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<void> {
Expand Down

0 comments on commit ec8fdf0

Please sign in to comment.