fix: withdraw/deposit converts from BZZ (10^16 base units) (#66)

* refactor: added toBZZbaseUnit function

* feat: added utility for attesting value is BZZ convertible to base units

* fix: conversion from 15 to 16 decimal places, added unsafe versions

* refactor: withdraw modal uses the safe conversion from BZZ

* refactor: added BigNumber and Token class to handle BZZ digits correctly

* refactor: extract deposit and withdraw functionality into single modal

* test: added tests for Token

* chore: removed unused component

* chore: addressed PR review, token decimal is now integer 0-18

* chore: added comment to clarify the value restriction on token amount
This commit is contained in:
Vojtech Simetka
2021-04-13 12:26:29 +02:00
committed by GitHub
parent 343e388498
commit 825772cf73
19 changed files with 411 additions and 238 deletions
-80
View File
@@ -1,80 +0,0 @@
import React, { ReactElement } from 'react'
import Button from '@material-ui/core/Button'
import Input from '@material-ui/core/Input'
import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle'
import { Snackbar } from '@material-ui/core'
import { beeDebugApi } from '../services/bee'
export default function DepositModal(): ReactElement {
const [open, setOpen] = React.useState(false)
const [amount, setAmount] = React.useState(BigInt(0))
const [showToast, setToastVisibility] = React.useState(false)
const [toastContent, setToastContent] = React.useState('')
const handleClickOpen = () => {
setOpen(true)
}
const handleClose = () => {
setOpen(false)
}
const handleWithdraw = () => {
if (amount > 0) {
beeDebugApi.chequebook
.deposit(amount)
.then(res => {
setOpen(false)
handleToast(`Successful Deposit. Transaction ${res.transactionHash}`)
})
.catch(() => {
handleToast('Error with Deposit')
})
} else {
handleToast('Must be amount of greater than 0')
}
}
const handleToast = (text: string) => {
setToastContent(text)
setToastVisibility(true)
setTimeout(() => setToastVisibility(false), 7000)
}
return (
<div>
<Button variant="outlined" color="primary" onClick={handleClickOpen} style={{ marginLeft: '7px' }}>
Deposit
</Button>
<Snackbar anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={showToast} message={toastContent} />
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Deposit Funds</DialogTitle>
<DialogContent>
<DialogContentText>Specify the amount you would like to deposit to your node.</DialogContentText>
<Input
autoFocus
margin="dense"
id="name"
type="number"
placeholder="Amount"
fullWidth
onChange={e => setAmount(BigInt(e.target.value))}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<Button onClick={handleWithdraw} color="primary">
Deposit
</Button>
</DialogActions>
</Dialog>
</div>
)
}
-46
View File
@@ -1,46 +0,0 @@
import React from 'react'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
import { Paper, InputBase, IconButton } from '@material-ui/core'
import { Search } from '@material-ui/icons'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
padding: '2px 4px',
display: 'flex',
alignItems: 'center',
width: 400,
},
input: {
marginLeft: theme.spacing(1),
flex: 1,
},
iconButton: {
padding: 10,
},
divider: {
height: 28,
margin: 4,
},
}),
)
export default function SearchBar() {
const classes = useStyles()
return (
<div>
<Paper component="form" className={classes.root}>
<InputBase
className={classes.input}
placeholder="Enter hash e.g. 0773a91efd6547c754fc1d95fb1c62c7d1b47f959c2caa685dfec8736da95c1c"
inputProps={{ 'aria-label': 'search google maps' }}
/>
<IconButton type="submit" className={classes.iconButton} aria-label="search">
<Search />
</IconButton>
</Paper>
</div>
)
}
+118
View File
@@ -0,0 +1,118 @@
import { ReactElement, useState } from 'react'
import Button from '@material-ui/core/Button'
import Input from '@material-ui/core/Input'
import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle'
import { FormHelperText, Snackbar } from '@material-ui/core'
import { Token } from '../models/Token'
import type { BigNumber } from 'bignumber.js'
interface Props {
successMessage: string
errorMessage: string
dialogMessage: string
label: string
max?: BigNumber
min?: BigNumber
action: (amount: bigint) => Promise<{ transactionHash: string }>
}
export default function WithdrawModal({
successMessage,
errorMessage,
dialogMessage,
min,
max,
label,
action,
}: Props): ReactElement {
const [open, setOpen] = useState(false)
const [amount, setAmount] = useState('')
const [amountToken, setAmountToken] = useState<Token | null>(null)
const [amountError, setAmountError] = useState<Error | null>(null)
const [showToast, setToastVisibility] = useState(false)
const [toastContent, setToastContent] = useState('')
const handleClickOpen = () => {
setOpen(true)
}
const handleClose = () => {
setOpen(false)
}
const handleAction = async () => {
if (amountToken === null) return
try {
const { transactionHash } = await action(amountToken.toBigInt)
setOpen(false)
handleToast(`${successMessage} Transaction ${transactionHash}`)
} catch (e) {
handleToast(`${errorMessage} Error: ${e.message}`)
}
}
const handleToast = (text: string) => {
setToastContent(text)
setToastVisibility(true)
setTimeout(() => setToastVisibility(false), 7000)
}
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
const value = e.target.value
setAmount(value)
setAmountError(null)
try {
const t = Token.fromDecimal(value)
setAmountToken(t)
if (min && t.toDecimal.isLessThan(min)) setAmountError(new Error(`Needs to be more than ${min}`))
if (max && t.toDecimal.isGreaterThan(max)) setAmountError(new Error(`Needs to be less than ${max}`))
} catch (e) {
setAmountError(e)
}
}
return (
<div>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
{label}
</Button>
<Snackbar anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={showToast} message={toastContent} />
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">{label}</DialogTitle>
<DialogContent>
<DialogContentText>{dialogMessage}</DialogContentText>
<Input
autoFocus
margin="dense"
id="name"
type="text"
placeholder="Amount"
fullWidth
value={amount}
onChange={handleChange}
/>
{amountError && (
<FormHelperText error>
Please provide valid BZZ amount (max 16 decimals). Error: {amountError.message}
</FormHelperText>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<Button onClick={handleAction} color="primary">
{label}
</Button>
</DialogActions>
</Dialog>
</div>
)
}
-81
View File
@@ -1,81 +0,0 @@
import { ReactElement, useState } from 'react'
import Button from '@material-ui/core/Button'
import Input from '@material-ui/core/Input'
import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle'
import { Snackbar } from '@material-ui/core'
import { beeDebugApi } from '../services/bee'
export default function WithdrawlModal(): ReactElement {
const [open, setOpen] = useState(false)
const [amount, setAmount] = useState(BigInt(0))
const [showToast, setToastVisibility] = useState(false)
const [toastContent, setToastContent] = useState('')
const handleClickOpen = () => {
setOpen(true)
}
const handleClose = () => {
setOpen(false)
}
const handleWithdraw = () => {
if (amount > 0) {
beeDebugApi.chequebook
.withdraw(amount)
.then(res => {
setOpen(false)
handleToast(`Successful withdrawl. Transaction ${res.transactionHash}`)
})
.catch(() => {
// FIXME: should probably detail the error
handleToast('Error with withdrawing')
})
} else {
handleToast('Must be amount of greater than 0')
}
}
const handleToast = (text: string) => {
setToastContent(text)
setToastVisibility(true)
setTimeout(() => setToastVisibility(false), 7000)
}
return (
<div>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
Withdraw
</Button>
<Snackbar anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={showToast} message={toastContent} />
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Withdraw Funds</DialogTitle>
<DialogContent>
<DialogContentText>Specify the amount you would like to withdraw from your node.</DialogContentText>
<Input
autoFocus
margin="dense"
id="name"
type="number"
placeholder="Amount"
fullWidth
onChange={e => setAmount(BigInt(e.target.value))}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<Button onClick={handleWithdraw} color="primary">
Withdraw
</Button>
</DialogActions>
</Dialog>
</div>
)
}