Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: postage stamps support #115

Merged
merged 8 commits into from
Jun 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21,718 changes: 21,640 additions & 78 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@
},
"dependencies": {
"@ethersphere/bee-js": "^0.9.0",
"@material-ui/core": "^4.11.3",
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.57",
"@types/react-router": "^5.1.13",
"@types/react-router-dom": "^5.1.7",
"axios": "^0.21.1",
"bignumber.js": "^9.0.1",
"feather-icons": "^4.28.0",
"formik": "^2.2.8",
"formik-material-ui": "^3.0.1",
"material-ui-dropzone": "^3.5.0",
"opener": "^1.5.2",
"qrcode.react": "^1.0.1",
Expand Down
13 changes: 9 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import CssBaseline from '@material-ui/core/CssBaseline'

import BaseRouter from './routes/routes'
import { lightTheme, darkTheme } from './theme'
import { Provider as StampsProvider } from './providers/Stamps'

const App = (): ReactElement => {
const [themeMode, toggleThemeMode] = useState('light')
Expand All @@ -33,10 +34,14 @@ const App = (): ReactElement => {
return (
<div className="App">
<ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}>
<CssBaseline />
<Router>
<BaseRouter />
</Router>
<StampsProvider>
<>
<CssBaseline />
<Router>
<BaseRouter />
</Router>
</>
</StampsProvider>
</ThemeProvider>
</div>
)
Expand Down
23 changes: 23 additions & 0 deletions src/components/LastUpdate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ReactElement, useEffect, useState } from 'react'

interface Props {
date: number | null
}

export default function LastUpdate({ date }: Props): ReactElement {
const [duration, setDuration] = useState<string>('never')

const refresh = () => {
if (!date) setDuration('never')
else setDuration(`${((Date.now() - date) / 1000).toFixed()} seconds ago`)
}

useEffect(() => {
refresh()
const i = setInterval(refresh, 1000)

return () => clearInterval(i)
}, [date])

return <span>Last Update: {duration}</span>
}
File renamed without changes.
8 changes: 7 additions & 1 deletion src/components/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Link, RouteComponentProps } from 'react-router-dom'
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
import { ListItemText, ListItemIcon, ListItem, Divider, List, Drawer, Link as MUILink } from '@material-ui/core'
import { OpenInNewSharp } from '@material-ui/icons'
import { Activity, FileText, DollarSign, Share2, Settings } from 'react-feather'
import { Activity, FileText, DollarSign, Share2, Settings, Layers } from 'react-feather'

import SwarmLogoOrange from '../assets/swarm-logo-orange.svg'
import { Health } from '@ethersphere/bee-js'
Expand All @@ -24,6 +24,12 @@ const navBarItems = [
path: '/files/',
icon: FileText,
},
{
label: 'Stamps',
id: 'stamps',
path: '/stamps/',
icon: Layers,
},
{
label: 'Accounting',
id: 'accounting',
Expand Down
25 changes: 25 additions & 0 deletions src/hooks/apiHooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Peer,
Topology,
LastChequesForPeerResponse,
PostageBatch,
} from '@ethersphere/bee-js'

import { beeDebugApi, beeApi } from '../services/bee'
Expand Down Expand Up @@ -429,3 +430,27 @@ export const useLatestBeeRelease = (): LatestBeeReleaseHook => {

return { latestBeeRelease, isLoadingLatestBeeRelease, error }
}

export interface GetPostageStampsHook {
postageStamps: PostageBatch[] | null
isLoading: boolean
error: Error | null
}

export const useGetPostageStamps = (): GetPostageStampsHook => {
const [postageStamps, setPostageStamps] = useState<PostageBatch[] | null>(null)
const [isLoading, setLoading] = useState<boolean>(false)
const [error, setError] = useState<Error | null>(null)

useEffect(() => {
beeApi.stamps
.getPostageStamps()
.then(setPostageStamps)
.catch(setError)
.finally(() => {
setLoading(false)
})
}, [])

return { postageStamps, isLoading, error }
}
2 changes: 1 addition & 1 deletion src/pages/accounting/BalancesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Table, TableBody, TableCell, TableContainer, TableRow, TableHead, Paper

import ClipboardCopy from '../../components/ClipboardCopy'
import CashoutModal from '../../components/CashoutModal'
import PeerDetailDrawer from './PeerDetail'
import PeerDetailDrawer from '../../components/PeerDetail'
import { Accounting } from '../../hooks/accounting'

const useStyles = makeStyles({
Expand Down
157 changes: 157 additions & 0 deletions src/pages/stamps/CreatePostageStampModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React, { ReactElement, useContext } from 'react'
import Button from '@material-ui/core/Button'
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 CircularProgress from '@material-ui/core/CircularProgress'
import DialogTitle from '@material-ui/core/DialogTitle'
import BigNumber from 'bignumber.js'
import { FormikHelpers, Form, Field, Formik } from 'formik'
import { TextField } from 'formik-material-ui'
import { beeApi } from '../../services/bee'
import { Context } from '../../providers/Stamps'
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'

interface FormValues {
depth?: string
amount?: string
label?: string
}
type FormErrors = Partial<FormValues>
const initialFormValues: FormValues = {
depth: '',
amount: '',
label: '',
}

const useStyles = makeStyles((theme: Theme) =>
createStyles({
wrapper: {
margin: theme.spacing(1),
position: 'relative',
},
field: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
buttonProgress: {
position: 'absolute',
top: '50%',
left: '50%',
marginTop: -12,
marginBottom: -12,
},
}),
)

interface Props {
label?: string
}

export default function FormDialog({ label }: Props): ReactElement {
const classes = useStyles()
const [open, setOpen] = React.useState(false)
const { refresh } = useContext(Context)
const handleClickOpen = () => setOpen(true)
const handleClose = () => setOpen(false)

return (
<Formik
initialValues={initialFormValues}
onSubmit={async (values: FormValues, actions: FormikHelpers<FormValues>) => {
try {
if (!values.depth) return

const amount = BigInt(values.amount)
const depth = Number.parseInt(values.depth)
const options = values.label ? { label: values.label } : undefined
await beeApi.stamps.buyPostageStamp(amount, depth, options)
actions.resetForm()
await refresh()
handleClose()
} catch (e) {
// TODO: trigger notification with notistack
console.error(`${e.message}`) // eslint-disable-line
actions.setSubmitting(false)
}
}}
validate={(values: FormValues) => {
const errors: FormErrors = {}

// Depth
if (!values.depth) errors.depth = 'Required field'
else {
const depth = new BigNumber(values.depth)

if (!depth.isInteger()) errors.depth = 'Depth must be an integer'
else if (depth.isLessThan(16)) errors.depth = 'Minimal depth is 16'
else if (depth.isGreaterThan(255)) errors.depth = 'Depth has to be at most 255'
}

// Amount
if (!values.amount) errors.amount = 'Required field'
else {
const amount = new BigNumber(values.amount)

if (!amount.isInteger()) errors.amount = 'Amount must be an integer'
else if (amount.isLessThanOrEqualTo(0)) errors.amount = 'Amount must be greater than 0'
}

// Label
if (values.label && !/^[0-9a-z]*$/i.test(values.label)) errors.label = 'Label must be an alphanumeric string'

return errors
}}
>
{({ submitForm, isValid, isSubmitting }) => (
<Form>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
{label || 'Buy Postage Stamp'}
{isSubmitting && <CircularProgress size={24} className={classes.buttonProgress} />}
</Button>
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Purchase new postage stamp</DialogTitle>
<DialogContent>
<DialogContentText>
Provide the depth, amount and optionally the label of the postage stamp. Please refer to the{' '}
<a href="https://docs.ethswarm.org/docs/access-the-swarm/keep-your-data-alive" target="blank">
official bee docs
</a>{' '}
to understand these values.
</DialogContentText>
<Field
component={TextField}
required
name="depth"
autoFocus
label="Depth"
fullWidth
className={classes.field}
/>
<Field component={TextField} required name="amount" label="Amount" fullWidth className={classes.field} />
<Field component={TextField} name="label" label="Label" fullWidth className={classes.field} />
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<div className={classes.wrapper}>
<Button
color="primary"
disabled={isSubmitting || !isValid}
type="submit"
variant="contained"
onClick={submitForm}
>
Create
{isSubmitting && <CircularProgress size={24} className={classes.buttonProgress} />}
</Button>
</div>
</DialogActions>
</Dialog>
</Form>
)}
</Formik>
)
}
55 changes: 55 additions & 0 deletions src/pages/stamps/StampsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { ReactElement } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import { Table, TableBody, TableCell, TableContainer, TableRow, TableHead, Paper } from '@material-ui/core'

import ClipboardCopy from '../../components/ClipboardCopy'
import PeerDetailDrawer from '../../components/PeerDetail'
import { PostageBatch } from '@ethersphere/bee-js'

const useStyles = makeStyles({
table: {
minWidth: 650,
},
values: {
textAlign: 'right',
fontFamily: 'monospace, monospace',
},
})
interface Props {
postageStamps: PostageBatch[] | null
}

function StampsTable({ postageStamps }: Props): ReactElement | null {
if (postageStamps === null) return null
const classes = useStyles()

return (
<TableContainer component={Paper}>
<Table className={classes.table} size="small" aria-label="Balances Table">
<TableHead>
<TableRow>
<TableCell>Batch ID</TableCell>
<TableCell align="right">Utilization</TableCell>
</TableRow>
</TableHead>
<TableBody>
{postageStamps.map(({ batchID, utilization }) => (
<TableRow key={batchID}>
<TableCell>
<div style={{ display: 'flex' }}>
<small>
<PeerDetailDrawer peerId={batchID} />
</small>
<ClipboardCopy value={batchID} />
</div>
</TableCell>
<TableCell className={classes.values}>{utilization}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)
}

export default StampsTable
Loading