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:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user