feat: postage stamps support (#115)
* chore: release 0.3.0 * feat: added postage stamp table to list all stamps * feat: postage stamp modal to purchase stamps * feat: postage stamps provider * chore: added formik * chore: proper form state handling * chore: revert accidental release inclusion * chore: polishing identified when developing the upload functionality Co-authored-by: bee-worker <70210089+bee-worker@users.noreply.github.com>
This commit is contained in:
Generated
+21640
-78
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -25,7 +25,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ethersphere/bee-js": "^0.9.0",
|
"@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/icons": "^4.11.2",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.57",
|
"@material-ui/lab": "^4.0.0-alpha.57",
|
||||||
"@types/react-router": "^5.1.13",
|
"@types/react-router": "^5.1.13",
|
||||||
@@ -33,6 +33,8 @@
|
|||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"bignumber.js": "^9.0.1",
|
"bignumber.js": "^9.0.1",
|
||||||
"feather-icons": "^4.28.0",
|
"feather-icons": "^4.28.0",
|
||||||
|
"formik": "^2.2.8",
|
||||||
|
"formik-material-ui": "^3.0.1",
|
||||||
"material-ui-dropzone": "^3.5.0",
|
"material-ui-dropzone": "^3.5.0",
|
||||||
"opener": "^1.5.2",
|
"opener": "^1.5.2",
|
||||||
"qrcode.react": "^1.0.1",
|
"qrcode.react": "^1.0.1",
|
||||||
|
|||||||
+9
-4
@@ -7,6 +7,7 @@ import CssBaseline from '@material-ui/core/CssBaseline'
|
|||||||
|
|
||||||
import BaseRouter from './routes/routes'
|
import BaseRouter from './routes/routes'
|
||||||
import { lightTheme, darkTheme } from './theme'
|
import { lightTheme, darkTheme } from './theme'
|
||||||
|
import { Provider as StampsProvider } from './providers/Stamps'
|
||||||
|
|
||||||
const App = (): ReactElement => {
|
const App = (): ReactElement => {
|
||||||
const [themeMode, toggleThemeMode] = useState('light')
|
const [themeMode, toggleThemeMode] = useState('light')
|
||||||
@@ -33,10 +34,14 @@ const App = (): ReactElement => {
|
|||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}>
|
<ThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}>
|
||||||
<CssBaseline />
|
<StampsProvider>
|
||||||
<Router>
|
<>
|
||||||
<BaseRouter />
|
<CssBaseline />
|
||||||
</Router>
|
<Router>
|
||||||
|
<BaseRouter />
|
||||||
|
</Router>
|
||||||
|
</>
|
||||||
|
</StampsProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { Link, RouteComponentProps } from 'react-router-dom'
|
|||||||
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
|
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
|
||||||
import { ListItemText, ListItemIcon, ListItem, Divider, List, Drawer, Link as MUILink } from '@material-ui/core'
|
import { ListItemText, ListItemIcon, ListItem, Divider, List, Drawer, Link as MUILink } from '@material-ui/core'
|
||||||
import { OpenInNewSharp } from '@material-ui/icons'
|
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 SwarmLogoOrange from '../assets/swarm-logo-orange.svg'
|
||||||
import { Health } from '@ethersphere/bee-js'
|
import { Health } from '@ethersphere/bee-js'
|
||||||
@@ -24,6 +24,12 @@ const navBarItems = [
|
|||||||
path: '/files/',
|
path: '/files/',
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Stamps',
|
||||||
|
id: 'stamps',
|
||||||
|
path: '/stamps/',
|
||||||
|
icon: Layers,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Accounting',
|
label: 'Accounting',
|
||||||
id: 'accounting',
|
id: 'accounting',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Peer,
|
Peer,
|
||||||
Topology,
|
Topology,
|
||||||
LastChequesForPeerResponse,
|
LastChequesForPeerResponse,
|
||||||
|
PostageBatch,
|
||||||
} from '@ethersphere/bee-js'
|
} from '@ethersphere/bee-js'
|
||||||
|
|
||||||
import { beeDebugApi, beeApi } from '../services/bee'
|
import { beeDebugApi, beeApi } from '../services/bee'
|
||||||
@@ -429,3 +430,27 @@ export const useLatestBeeRelease = (): LatestBeeReleaseHook => {
|
|||||||
|
|
||||||
return { latestBeeRelease, isLoadingLatestBeeRelease, error }
|
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 }
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Table, TableBody, TableCell, TableContainer, TableRow, TableHead, Paper
|
|||||||
|
|
||||||
import ClipboardCopy from '../../components/ClipboardCopy'
|
import ClipboardCopy from '../../components/ClipboardCopy'
|
||||||
import CashoutModal from '../../components/CashoutModal'
|
import CashoutModal from '../../components/CashoutModal'
|
||||||
import PeerDetailDrawer from './PeerDetail'
|
import PeerDetailDrawer from '../../components/PeerDetail'
|
||||||
import { Accounting } from '../../hooks/accounting'
|
import { Accounting } from '../../hooks/accounting'
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { ReactElement, useContext, useEffect } from 'react'
|
||||||
|
import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'
|
||||||
|
import { Container, CircularProgress } from '@material-ui/core'
|
||||||
|
|
||||||
|
import StampsTable from './StampsTable'
|
||||||
|
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||||
|
import CreatePostageStampModal from './CreatePostageStampModal'
|
||||||
|
import LastUpdate from '../../components/LastUpdate'
|
||||||
|
|
||||||
|
import { useApiHealth, useDebugApiHealth } from '../../hooks/apiHooks'
|
||||||
|
import { Context } from '../../providers/Stamps'
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
root: {
|
||||||
|
width: '100%',
|
||||||
|
display: 'grid',
|
||||||
|
rowGap: theme.spacing(2),
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
columnGap: theme.spacing(1),
|
||||||
|
rowGap: theme.spacing(1),
|
||||||
|
flex: '0 1 auto',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export default function Accounting(): ReactElement {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const { health, isLoadingHealth } = useApiHealth()
|
||||||
|
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
|
||||||
|
const { stamps, isLoading, error, lastUpdate, start, stop } = useContext(Context)
|
||||||
|
useEffect(() => {
|
||||||
|
start()
|
||||||
|
|
||||||
|
return () => stop()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (isLoadingHealth || isLoadingNodeHealth) {
|
||||||
|
return (
|
||||||
|
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeHealth?.status !== 'ok' || !health) return <TroubleshootConnectionCard />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
{error && (
|
||||||
|
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||||
|
Error loading postage stamps details: {error.message}
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
{!error && (
|
||||||
|
<>
|
||||||
|
<div className={classes.actions}>
|
||||||
|
<CreatePostageStampModal />
|
||||||
|
<LastUpdate date={lastUpdate} />
|
||||||
|
<div style={{ height: '5px' }}>{isLoading && <CircularProgress />}</div>
|
||||||
|
</div>
|
||||||
|
<StampsTable postageStamps={stamps} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { PostageBatch } from '@ethersphere/bee-js'
|
||||||
|
import { createContext, ReactChild, ReactElement, useEffect, useState } from 'react'
|
||||||
|
import { beeApi } from '../services/bee'
|
||||||
|
|
||||||
|
interface ContextInterface {
|
||||||
|
stamps: PostageBatch[] | null
|
||||||
|
error: Error | null
|
||||||
|
isLoading: boolean
|
||||||
|
lastUpdate: number | null
|
||||||
|
start: (frequency?: number) => void
|
||||||
|
stop: () => void
|
||||||
|
refresh: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValues: ContextInterface = {
|
||||||
|
stamps: null,
|
||||||
|
error: null,
|
||||||
|
isLoading: false,
|
||||||
|
lastUpdate: null,
|
||||||
|
start: () => {}, // eslint-disable-line
|
||||||
|
stop: () => {}, // eslint-disable-line
|
||||||
|
refresh: () => Promise.reject(),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Context = createContext<ContextInterface>(initialValues)
|
||||||
|
export const Consumer = Context.Consumer
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactChild
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Provider({ children }: Props): ReactElement {
|
||||||
|
const [stamps, setStamps] = useState<PostageBatch[] | null>(initialValues.stamps)
|
||||||
|
const [error, setError] = useState<Error | null>(initialValues.error)
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading)
|
||||||
|
const [lastUpdate, setLastUpdate] = useState<number | null>(initialValues.lastUpdate)
|
||||||
|
const [frequency, setFrequency] = useState<number | null>(null)
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
// Don't want to refresh when already refreshing
|
||||||
|
if (isLoading) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
const stamps = await beeApi.stamps.getPostageStamps()
|
||||||
|
|
||||||
|
setStamps(stamps)
|
||||||
|
setLastUpdate(Date.now())
|
||||||
|
} catch (e) {
|
||||||
|
setError(e)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = (freq = 30000) => setFrequency(freq)
|
||||||
|
const stop = () => setFrequency(null)
|
||||||
|
|
||||||
|
// Start the update loop
|
||||||
|
useEffect(() => {
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
// Start autorefresh only if the frequency is set
|
||||||
|
if (frequency) {
|
||||||
|
const interval = setInterval(refresh, frequency)
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}
|
||||||
|
}, [frequency])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context.Provider value={{ stamps, error, isLoading, lastUpdate, start, stop, refresh }}>
|
||||||
|
{children}
|
||||||
|
</Context.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,11 +7,12 @@ import AppRoute from './AppRoute'
|
|||||||
import Dashboard from '../layout/Dashboard'
|
import Dashboard from '../layout/Dashboard'
|
||||||
|
|
||||||
// pages
|
// pages
|
||||||
import Status from '../pages/status/index'
|
import Status from '../pages/status'
|
||||||
import Files from '../pages/files/index'
|
import Files from '../pages/files'
|
||||||
import Peers from '../pages/peers/index'
|
import Peers from '../pages/peers'
|
||||||
import Accounting from '../pages/accounting/index'
|
import Accounting from '../pages/accounting'
|
||||||
import Settings from '../pages/settings/index'
|
import Settings from '../pages/settings'
|
||||||
|
import Stamps from '../pages/stamps'
|
||||||
|
|
||||||
const BaseRouter = (): ReactElement => (
|
const BaseRouter = (): ReactElement => (
|
||||||
<Switch>
|
<Switch>
|
||||||
@@ -20,6 +21,7 @@ const BaseRouter = (): ReactElement => (
|
|||||||
<AppRoute exact path="/peers/" layout={Dashboard} component={Peers} />
|
<AppRoute exact path="/peers/" layout={Dashboard} component={Peers} />
|
||||||
<AppRoute exact path="/accounting/" layout={Dashboard} component={Accounting} />
|
<AppRoute exact path="/accounting/" layout={Dashboard} component={Accounting} />
|
||||||
<AppRoute exact path="/settings/" layout={Dashboard} component={Settings} />
|
<AppRoute exact path="/settings/" layout={Dashboard} component={Settings} />
|
||||||
|
<AppRoute exact path="/stamps/" layout={Dashboard} component={Stamps} />
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
NodeAddresses,
|
NodeAddresses,
|
||||||
Peer,
|
Peer,
|
||||||
PingResponse,
|
PingResponse,
|
||||||
|
PostageBatch,
|
||||||
|
PostageBatchOptions,
|
||||||
Reference,
|
Reference,
|
||||||
Topology,
|
Topology,
|
||||||
WithdrawTokensResponse,
|
WithdrawTokensResponse,
|
||||||
@@ -41,6 +43,14 @@ export const beeApi = {
|
|||||||
return beeJSClient().downloadFile(hash)
|
return beeJSClient().downloadFile(hash)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
stamps: {
|
||||||
|
getPostageStamps(): Promise<PostageBatch[]> {
|
||||||
|
return beeJSClient().getAllPostageBatch()
|
||||||
|
},
|
||||||
|
buyPostageStamp(amount: bigint, depth: number, options: PostageBatchOptions = {}): Promise<Address> {
|
||||||
|
return beeJSClient().createPostageBatch(amount, depth, options)
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const beeDebugApi = {
|
export const beeDebugApi = {
|
||||||
|
|||||||
Reference in New Issue
Block a user