feat: sync and update with all changes from solar-punk-ltd fork (#730)

* fix: swap error caused by invalid id and batchcount
* fix: enhance creation messages for admin drive and user drives
* fix: identity and wallet creation
* fix: asset preview types
* fix: fm search unicode text
* fix: feed identity and stamp usage
* fix: ui display changes
* fix: stamp buy and dilute
* fix: vite polyfill warning for stream
* fix: standard mode postage stamp purchase reserves incorrect size and duration
* fix: add syncing message for Bee node and update page state handling
* refactor: stamp depth and amount validation

---------

Co-authored-by: Balint Ujvari <balint.ujvari@solarpunk.buzz>
Co-authored-by: Bálint Ujvári <58116288+bosi95@users.noreply.github.com>
Co-authored-by: rolandlor <33499567+rolandlor@users.noreply.github.com>
This commit is contained in:
Ferenc Sárai
2026-04-02 14:53:20 +02:00
committed by GitHub
parent 4848b5be97
commit cb5adfe031
41 changed files with 627 additions and 380 deletions
+1 -1
View File
@@ -104,7 +104,7 @@ export function AccountFeeds(): ReactElement {
{x.feedHash && <ExpandableListItemKey label="Feed hash" value={x.feedHash} />}
<Box mt={0.75}>
<ExpandableListItemActions>
<SwarmButton onClick={() => viewFeed(x.uuid)} iconType={Info}>
<SwarmButton onClick={() => viewFeed(x.uuid)} iconType={Info} disabled={Boolean(!x.feedHash)}>
View Feed Page
</SwarmButton>
<SwarmButton onClick={() => onShowExport(x)} iconType={Download}>
+41 -11
View File
@@ -1,5 +1,6 @@
import { NULL_TOPIC } from '@ethersphere/bee-js'
import { NULL_TOPIC, PostageBatch } from '@ethersphere/bee-js'
import { Box, Grid, Typography } from '@mui/material'
import { Wallet } from 'ethers'
import { Form, Formik } from 'formik'
import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useState } from 'react'
@@ -12,7 +13,7 @@ import ExpandableListItemActions from '../../components/ExpandableListItemAction
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import { HistoryHeader } from '../../components/HistoryHeader'
import { SwarmButton } from '../../components/SwarmButton'
import { SwarmSelect } from '../../components/SwarmSelect'
import { SelectEvent, SwarmSelect } from '../../components/SwarmSelect'
import { SwarmTextInput } from '../../components/SwarmTextInput'
import { Context as FeedsContext, IdentityType } from '../../providers/Feeds'
import { Context as SettingsContext } from '../../providers/Settings'
@@ -34,7 +35,8 @@ const initialValues: FormValues = {
export default function CreateNewFeed(): ReactElement {
const { beeApi } = useContext(SettingsContext)
const { identities, setIdentities } = useContext(FeedsContext)
const [loading, setLoading] = useState(false)
const [identityType, setIdentityType] = useState<IdentityType>(IdentityType.PrivateKey)
const [loading, setLoading] = useState<boolean>(false)
const { enqueueSnackbar } = useSnackbar()
const navigate = useNavigate()
@@ -48,11 +50,24 @@ export default function CreateNewFeed(): ReactElement {
return
}
const wallet = generateWallet()
const stamps = await beeApi.getPostageBatches()
let stamps: PostageBatch[] = []
let wallet: Wallet
try {
wallet = generateWallet()
stamps = (await beeApi.getPostageBatches()).filter(s => s.usable)
} catch (err) {
// eslint-disable-next-line no-console
console.log(err)
enqueueSnackbar(<span>Error during wallet generation or postage stamp retrieval!</span>, { variant: 'error' })
setLoading(false)
return
}
if (!stamps || !stamps.length) {
enqueueSnackbar(<span>No stamp available</span>, { variant: 'error' })
enqueueSnackbar(<span>No usable stamp available</span>, { variant: 'error' })
setLoading(false)
return
@@ -65,17 +80,29 @@ export default function CreateNewFeed(): ReactElement {
return
}
const identity = await convertWalletToIdentity(wallet, values.type, values.identityName, values.password)
persistIdentity(identities, identity)
setIdentities(identities)
navigate(ROUTES.ACCOUNT_FEEDS)
setLoading(false)
try {
const identity = await convertWalletToIdentity(wallet, values.type, values.identityName, values.password)
persistIdentity(identities, identity)
setIdentities(identities)
navigate(ROUTES.ACCOUNT_FEEDS)
} catch (err) {
// eslint-disable-next-line no-console
console.log(err)
enqueueSnackbar(<span>Error identity creation!</span>, { variant: 'error' })
} finally {
setLoading(false)
}
}
function cancel() {
navigate(-1)
}
function onIdentityTypeChange(event: SelectEvent) {
const type = event.target.value as IdentityType
setIdentityType(type)
}
return (
<div>
<HistoryHeader>Create new feed</HistoryHeader>
@@ -102,10 +129,13 @@ export default function CreateNewFeed(): ReactElement {
<SwarmSelect
formik
name="type"
label={'type'}
value={identityType}
options={[
{ label: 'Keypair Only', value: IdentityType.PrivateKey },
{ label: 'Password Protected', value: IdentityType.V3 },
]}
onChange={onIdentityTypeChange}
/>
</Box>
{values.type === IdentityType.V3 && <SwarmTextInput name="password" label="Password" password formik />}
+18 -9
View File
@@ -26,8 +26,8 @@ export default function UpdateFeed(): ReactElement {
const { status } = useContext(BeeContext)
const { hash } = useParams()
const [selectedStamp, setSelectedStamp] = useState<string | null>(null)
const [selectedIdentity, setSelectedIdentity] = useState<Identity | null>(null)
const [selectedStamp, setSelectedStamp] = useState<string | null>(stamps ? stamps[0]?.batchID.toHex() : null)
const [selectedIdentity, setSelectedIdentity] = useState<Identity | null>(identities[0] ?? null)
const [loading, setLoading] = useState(false)
const { enqueueSnackbar } = useSnackbar()
const [showPasswordPrompt, setShowPasswordPrompt] = useState(false)
@@ -119,19 +119,28 @@ export default function UpdateFeed(): ReactElement {
<HistoryHeader>Update feed</HistoryHeader>
<Box mb={2}>
<Grid container>
<SwarmSelect
options={identities.map(x => ({ value: x.uuid, label: `${x.name} Website` }))}
onChange={onFeedChange}
label="Feed"
/>
{identities && identities.length ? (
<SwarmSelect
value={selectedIdentity?.uuid ?? ''}
options={identities.map(x => ({ value: x.uuid, label: `${x.name} Website` }))}
onChange={onFeedChange}
label="Feed"
/>
) : (
<Typography>You need to create an identiy first to be able to update its feed.</Typography>
)}
</Grid>
</Box>
<Box mb={4}>
<Grid container>
{stamps ? (
{stamps && stamps.length ? (
<SwarmSelect
options={stamps.map(x => ({ value: x.batchID.toHex(), label: x.batchID.toHex().slice(0, 8) }))}
value={selectedStamp ?? ''}
options={stamps.map(x => ({
value: x.batchID.toHex(),
label: x.label ? x.batchID.toHex().slice(0, 8) + ` (${x.label})` : x.batchID.toHex().slice(0, 8),
}))}
onChange={onStampChange}
label="Stamp"
/>
+1 -1
View File
@@ -104,7 +104,7 @@ export default function Feeds(): ReactElement {
{x.feedHash && <ExpandableListItemKey label="Feed hash" value={x.feedHash} />}
<Box mt={0.75}>
<ExpandableListItemActions>
<SwarmButton onClick={() => viewFeed(x.uuid)} iconType={Info}>
<SwarmButton onClick={() => viewFeed(x.uuid)} iconType={Info} disabled={Boolean(!x.feedHash)}>
View Feed Page
</SwarmButton>
<SwarmButton onClick={() => onShowExport(x)} iconType={Download}>
+53 -23
View File
@@ -1,26 +1,26 @@
import { DriveInfo, FileManagerBase } from '@solarpunkltd/file-manager-lib'
import { ReactElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { AdminStatusBar } from '../../modules/filemanager/components/AdminStatusBar/AdminStatusBar'
import { Button } from '../../modules/filemanager/components/Button/Button'
import { ConfirmModal } from '../../modules/filemanager/components/ConfirmModal/ConfirmModal'
import { ErrorModal } from '../../modules/filemanager/components/ErrorModal/ErrorModal'
import { FileBrowser } from '../../modules/filemanager/components/FileBrowser/FileBrowser'
import { FormbricksIntegration } from '../../modules/filemanager/components/FormbricksIntegration/FormbricksIntegration'
import { Header } from '../../modules/filemanager/components/Header/Header'
import { InitialModal } from '../../modules/filemanager/components/InitialModal/InitialModal'
import { PrivateKeyModal } from '../../modules/filemanager/components/PrivateKeyModal/PrivateKeyModal'
import { Sidebar } from '../../modules/filemanager/components/Sidebar/Sidebar'
import { getSignerPk, removeSignerPk } from '../../modules/filemanager/utils/common'
import { CheckState, Context as BeeContext } from '../../providers/Bee'
import { Context as FMContext } from '../../providers/FileManager'
import { BrowserPlatform, cacheClearUrls, detectBrowser } from '../../providers/Platform'
import { SearchProvider } from './SearchContext'
import { ViewProvider } from './ViewContext'
import './FileManager.scss'
import { AdminStatusBar } from '@/modules/filemanager/components/AdminStatusBar/AdminStatusBar'
import { Button } from '@/modules/filemanager/components/Button/Button'
import { ConfirmModal } from '@/modules/filemanager/components/ConfirmModal/ConfirmModal'
import { ErrorModal } from '@/modules/filemanager/components/ErrorModal/ErrorModal'
import { FileBrowser } from '@/modules/filemanager/components/FileBrowser/FileBrowser'
import { FormbricksIntegration } from '@/modules/filemanager/components/FormbricksIntegration/FormbricksIntegration'
import { Header } from '@/modules/filemanager/components/Header/Header'
import { InitialModal } from '@/modules/filemanager/components/InitialModal/InitialModal'
import { PrivateKeyModal } from '@/modules/filemanager/components/PrivateKeyModal/PrivateKeyModal'
import { Sidebar } from '@/modules/filemanager/components/Sidebar/Sidebar'
import { getSignerPk, removeSignerPk } from '@/modules/filemanager/utils/common'
import { CheckState, Context as BeeContext } from '@/providers/Bee'
import { Context as FMContext } from '@/providers/FileManager'
import { BrowserPlatform, cacheClearUrls, detectBrowser } from '@/providers/Platform'
function PrivateKeyModalBlock({ onSaved }: { onSaved: () => void }) {
return (
<div className="fm-main">
@@ -95,6 +95,22 @@ function LoadingBlock() {
)
}
function ChainSyncingBlock() {
return (
<div className="fm-main">
<div className="fm-loading" aria-live="polite">
<div className="fm-spinner" aria-hidden="true" />
<div className="fm-loading-title">Bee node is syncing</div>
<div className="fm-loading-subtitle">
Your Bee node is still syncing the postage batch state from the chain.
<br />
File Manager will be available once the sync is complete.
</div>
</div>
</div>
)
}
function ErrorModalBlock({ onClick, label }: { onClick: () => void; label: string }) {
return <ErrorModal label={label} onClick={onClick} />
}
@@ -157,6 +173,7 @@ enum PageState {
Loading = 'loading', // bee ready, pk present, FM init in progress
Reset = 'reset', // STATE_INVALID emitted and user has not yet acknowledged
InitError = 'init-error', // FM init completed with an error (non-reset case)
ChainSyncing = 'chain-syncing', // bee node is still syncing postage batch state from chain
Initial = 'initial', // FM ready but no admin stamp/drive → show InitialModal
AdminError = 'admin-error', // drive creation failed
Ready = 'ready', // fully operational
@@ -172,7 +189,7 @@ export function FileManagerPage(): ReactElement {
const [connectionErrorDismissed, setConnectionErrorDismissed] = useState<boolean>(false)
const [cacheHelpUrl, setCacheHelpUrl] = useState<string>(cacheClearUrls[BrowserPlatform.Chrome])
const { status } = useContext(BeeContext)
const { status, chainState } = useContext(BeeContext)
const { fm, initDone, shallReset, adminDrive, initializationError, notifyPkSaved } = useContext(FMContext)
useEffect(() => {
@@ -207,6 +224,8 @@ export function FileManagerPage(): ReactElement {
}, [isConnectionError])
const pageState = useMemo((): PageState => {
const isChainSyncing = chainState === null
if (!isBeeReady && !initDone) return PageState.Connecting
if (!hasPk) return PageState.NoPrivateKey
@@ -217,12 +236,15 @@ export function FileManagerPage(): ReactElement {
if (initializationError && !shallReset) return PageState.InitError
if (showAdminErrorModal) return PageState.AdminError
const hasAdminStamp = Boolean(fm?.adminStamp)
const hasAdminDrive = Boolean(adminDrive)
const setupIncomplete = !hasAdminStamp && !hasAdminDrive
if (!hasAdminStamp && !hasAdminDrive && !isCreationInProgress) return PageState.Initial
if (setupIncomplete && isChainSyncing) return PageState.ChainSyncing
if (showAdminErrorModal) return PageState.AdminError
if (setupIncomplete && !isCreationInProgress) return PageState.Initial
return PageState.Ready
}, [
@@ -236,6 +258,7 @@ export function FileManagerPage(): ReactElement {
fm,
adminDrive,
isCreationInProgress,
chainState,
])
const handlePrivateKeySaved = useCallback(() => {
@@ -255,6 +278,10 @@ export function FileManagerPage(): ReactElement {
return <LoadingBlock />
}
if (pageState === PageState.ChainSyncing) {
return <ChainSyncingBlock />
}
if (pageState === PageState.NoPrivateKey) {
return <PrivateKeyModalBlock onSaved={handlePrivateKeySaved} />
}
@@ -289,12 +316,15 @@ export function FileManagerPage(): ReactElement {
}
if (pageState === PageState.AdminError) {
const adminErrorLabel =
chainState === null
? 'Your Bee node is still syncing the postage batch state from the chain. Please wait for the sync to complete and try again.'
: errorMessage ||
'Error creating Admin Drive. Please try again. Possible causes include insufficient xDAI balance or a lost connection to the RPC.'
return (
<ErrorModalBlock
label={
errorMessage ||
'Error creating Admin Drive. Please try again. Possible causes include insufficient xDAI balance or a lost connection to the RPC.'
}
label={adminErrorLabel}
onClick={() => {
setAdminShowErrorModal(false)
setErrorMessage('')
+20 -8
View File
@@ -8,7 +8,7 @@ import { FitAudio } from '../../components/FitAudio'
import { FitImage } from '../../components/FitImage'
import { FitVideo } from '../../components/FitVideo'
import { shortenText } from '../../utils'
import { getHumanReadableFileSize } from '../../utils/file'
import { getHumanReadableFileSize, guessMime } from '../../utils/file'
import { shortenHash } from '../../utils/hash'
import { AssetIcon } from './AssetIcon'
@@ -18,16 +18,20 @@ interface Props {
metadata?: Metadata
}
const getPreviewElement = (previewUri?: string, metadata?: Metadata) => {
if (metadata?.isVideo) {
const getPreviewElement = (previewUri?: string, metadata?: Metadata, type?: string) => {
const isVideoType = Boolean(type && /.*\.(mp4|webm|ogv)$/i.test(type))
const isAudioType = Boolean(type && /.*\.(mp3|ogg|oga|wav|webm|m4a|aac|flac)$/i.test(type))
const isImageType = Boolean(type && /.*\.(jpg|jpeg|png|gif|webp|svg|ico)$/i.test(type))
if (metadata?.isVideo || isVideoType) {
return <FitVideo src={previewUri} maxWidth="250px" maxHeight="175px" />
}
if (metadata?.isAudio) {
if (metadata?.isAudio || isAudioType) {
return <FitAudio src={previewUri} maxWidth="250px" />
}
if (metadata?.isImage) {
if (metadata?.isImage || isImageType) {
return <FitImage maxWidth="250px" maxHeight="175px" alt="Upload Preview" src={previewUri} />
}
@@ -42,18 +46,26 @@ const getPreviewElement = (previewUri?: string, metadata?: Metadata) => {
return <AssetIcon icon={<File />} />
}
const getType = (metadata?: Metadata) => {
export const getType = (metadata?: Metadata): string => {
if (metadata?.isWebsite) return 'Website'
if (metadata?.type === 'folder') return 'Folder'
return metadata?.type
let metadataType = metadata?.type || 'unknown'
let typeFromExtension: string | undefined
if (metadataType === 'unknown' && metadata?.name) {
const { mime } = guessMime(metadata.name)
typeFromExtension = mime === 'application/octet-stream' ? 'file' : mime
}
return typeFromExtension || metadataType
}
// TODO: add optional prop for indexDocument when it is already known (e.g. downloading a manifest)
export function AssetPreview({ metadata, previewUri }: Props): ReactElement | null {
const previewElement = useMemo(() => getPreviewElement(previewUri, metadata), [metadata, previewUri])
const type = useMemo(() => getType(metadata), [metadata])
const previewElement = useMemo(() => getPreviewElement(previewUri, metadata, type), [metadata, type, previewUri])
return (
<Box mb={4}>
+3 -3
View File
@@ -16,7 +16,7 @@ import { ROUTES } from '../../routes'
import { determineHistoryName, LocalStorageKeys, putHistory } from '../../utils/localStorage'
import { loadManifest } from '../../utils/manifest'
import { AssetPreview } from './AssetPreview'
import { AssetPreview, getType } from './AssetPreview'
import { AssetSummary } from './AssetSummary'
import { AssetSyncing } from './AssetSyncing'
import { DownloadActionBar } from './DownloadActionBar'
@@ -46,7 +46,7 @@ export function Share(): ReactElement {
const count = Object.keys(entries).length
const isVideo = Boolean(indexDocument && /.*\.(mp4|webm|ogv)$/i.test(indexDocument))
const isAudio = Boolean(indexDocument && /.*\.(mp3|ogg|oga|wav|webm|m4a|aac|flac)$/i.test(indexDocument))
const isImage = Boolean(indexDocument && /.*\.(jpg|jpeg|png|gif|webp|svg)$/i.test(indexDocument))
const isImage = Boolean(indexDocument && /.*\.(jpg|jpeg|png|gif|webp|svg|ico)$/i.test(indexDocument))
if (isImage || isVideo || isAudio) {
setPreview(`${apiUrl}/bzz/${hash}`)
@@ -54,7 +54,7 @@ export function Share(): ReactElement {
setMetadata({
hash,
type: count > 1 ? 'folder' : 'unknown',
type: count > 1 ? 'folder' : getType(),
name: indexDocument || hash || '',
count,
isWebsite: Boolean(indexDocument && /.*\.html?$/i.test(indexDocument)),
+1 -1
View File
@@ -14,10 +14,10 @@ import { Context as FileContext } from '../../providers/File'
import { Context as SettingsContext } from '../../providers/Settings'
import { Context as StampsContext, EnrichedPostageBatch } from '../../providers/Stamps'
import { ROUTES } from '../../routes'
import { waitUntilStampUsable } from '../../utils'
import { detectIndexHtml, getAssetNameFromFiles, packageFile } from '../../utils/file'
import { persistIdentity, updateFeed } from '../../utils/identity'
import { LocalStorageKeys, putHistory } from '../../utils/localStorage'
import { waitUntilStampUsable } from '../../utils/stamp'
import { FeedPasswordDialog } from '../feeds/FeedPasswordDialog'
import { PostageStampAdvancedCreation } from '../stamps/PostageStampAdvancedCreation'
import { PostageStampSelector } from '../stamps/PostageStampSelector'
+2 -1
View File
@@ -1,3 +1,4 @@
import { BeeModes } from '@ethersphere/bee-js'
import { useContext } from 'react'
import { useNavigate } from 'react-router'
import Upload from 'remixicon-react/UploadLineIcon'
@@ -19,7 +20,7 @@ export function WalletInfoCard() {
)} xBZZ | ${walletBalance.nativeTokenBalance.toSignificantDigits(4)} xDAI`
}
if (nodeInfo?.beeMode && ['light', 'full', 'dev'].includes(nodeInfo.beeMode)) {
if (nodeInfo?.beeMode && [BeeModes.LIGHT, BeeModes.FULL, BeeModes.DEV].includes(nodeInfo.beeMode)) {
return (
<Card
buttonProps={{
@@ -1,4 +1,4 @@
import { PostageBatchOptions, Utils } from '@ethersphere/bee-js'
import { PostageBatchOptions, RedundancyLevel, Utils } from '@ethersphere/bee-js'
import { Box, Grid, IconButton, Typography } from '@mui/material'
import BigNumber from 'bignumber.js'
import { useSnackbar } from 'notistack'
@@ -11,12 +11,14 @@ import { makeStyles } from 'tss-react/mui'
import { SwarmButton } from '../../components/SwarmButton'
import { SwarmSelect } from '../../components/SwarmSelect'
import { SwarmTextInput } from '../../components/SwarmTextInput'
import { MAX_STAMP_DEPTH, MIN_STAMP_DEPTH } from '../../constants'
import { Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings'
import { Context as StampsContext } from '../../providers/Stamps'
import { ROUTES } from '../../routes'
import { secondsToTimeString } from '../../utils'
import { getHumanReadableFileSize } from '../../utils/file'
import { validateDepthInput } from '../../utils/stamp'
interface Props {
onFinished: () => void
@@ -80,7 +82,7 @@ export function PostageStampAdvancedCreation({ onFinished }: Props): ReactElemen
}
function getPrice(depth: number, amount: bigint): string {
const hasInvalidInput = amount <= 0 || isNaN(depth) || depth < 17 || depth > 255
const hasInvalidInput = amount <= 0 || isNaN(depth) || depth < MIN_STAMP_DEPTH || depth > MAX_STAMP_DEPTH
if (hasInvalidInput) {
return '-'
@@ -147,38 +149,15 @@ export function PostageStampAdvancedCreation({ onFinished }: Props): ReactElemen
setAmountInput(validAmountInput)
}
function validateDepthInput(depthInput: string) {
let validDepthInput = '0'
if (!depthInput) {
setDepthError('Required field')
} else {
const depth = new BigNumber(depthInput)
if (!depth.isInteger()) {
setDepthError('Depth must be an integer')
} else if (depth.isLessThan(17)) {
setDepthError('Minimal depth is 17')
} else if (depth.isGreaterThan(255)) {
setDepthError('Depth has to be at most 255')
} else {
setDepthError('')
validDepthInput = depthInput
}
}
setDepthInput(validDepthInput)
}
function renderStampVolumesInfo() {
const depth = parseInt(depthInput, 10)
if (depthError || isNaN(depth) || depth < 17 || depth > 255) {
if (depthError || isNaN(depth) || depth < MIN_STAMP_DEPTH || depth > MAX_STAMP_DEPTH) {
return '-'
}
const theoreticalMaximumVolume = getHumanReadableFileSize(Utils.getStampTheoreticalBytes(depth))
const effectiveVolume = getHumanReadableFileSize(Utils.getStampEffectiveBytes(depth))
const effectiveVolume = getHumanReadableFileSize(Utils.getStampEffectiveBytes(depth, false, RedundancyLevel.OFF))
return (
<Grid container alignItems="center" className={classes.stampVolumeWrapper}>
@@ -217,7 +196,11 @@ export function PostageStampAdvancedCreation({ onFinished }: Props): ReactElemen
</Typography>
</Box>
<Box mb={2}>
<SwarmTextInput name="depth" label="Depth" onChange={event => validateDepthInput(event.target.value)} />
<SwarmTextInput
name="depth"
label="Depth"
onChange={event => validateDepthInput(event.target.value, setDepthError, setDepthInput)}
/>
<Box mt={0.25} sx={{ bgcolor: '#f6f6f6' }} p={2}>
<Grid container justifyContent="space-between" alignItems="center">
<Typography>Corresponding file size</Typography>
@@ -242,7 +225,7 @@ export function PostageStampAdvancedCreation({ onFinished }: Props): ReactElemen
<Box mb={2}>
<SwarmSelect
label="Immutable"
value="No"
value={immutable ? 'Yes' : 'No'}
onChange={event => setImmutable(event.target.value === 'Yes')}
options={[
{ value: 'Yes', label: 'Yes' },
@@ -1,4 +1,4 @@
import { Duration, PostageBatchOptions, Size, Utils } from '@ethersphere/bee-js'
import { Duration, RedundancyLevel, Size, Utils } from '@ethersphere/bee-js'
import { Box, Button, Grid, Slider, Typography } from '@mui/material'
import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useState } from 'react'
@@ -8,10 +8,12 @@ import { makeStyles } from 'tss-react/mui'
import { SwarmButton } from '../../components/SwarmButton'
import { SwarmTextInput } from '../../components/SwarmTextInput'
import { Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings'
import { Context as StampsContext } from '../../providers/Stamps'
import { ROUTES } from '../../routes'
import { secondsToTimeString } from '../../utils'
import { validateDepthInput } from '../../utils/stamp'
interface Props {
onFinished: () => void
@@ -48,12 +50,17 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen
const { classes } = useStyles()
const { refresh } = useContext(StampsContext)
const { beeApi } = useContext(SettingsContext)
const { chainState } = useContext(BeeContext)
const [depthInput, setDepthInput] = useState<number>(Utils.getDepthForSize(Size.fromGigabytes(4)))
const [amountInput, setAmountInput] = useState<bigint>(Utils.getAmountForDuration(Duration.fromDays(30), 26500, 5))
const [labelInput, setLabelInput] = useState('')
const [submitting, setSubmitting] = useState(false)
const [buttonValue, setButtonValue] = useState(4)
const [depthError, setDepthError] = useState<string>('')
const [sliderValue, setSliderValue] = useState(30)
const pricePerBlockDefault = 24000
const currentPrice = chainState?.currentPrice ?? pricePerBlockDefault
const getBatchValue = (value: number) => {
return (
@@ -74,18 +81,18 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen
if (typeof newValue !== 'number') {
return
}
const amountValue = Utils.getAmountForDuration(Duration.fromDays(newValue), 26500, 5)
const amountValue = Utils.getAmountForDuration(Duration.fromDays(newValue), currentPrice, 5)
setAmountInput(amountValue)
setSliderValue(newValue)
}
const { enqueueSnackbar } = useSnackbar()
function getTtl(amount: bigint): string {
const pricePerBlock = 24000
return `${secondsToTimeString(
Utils.getStampDuration(amount, pricePerBlock, 5).toSeconds(),
)} (with price of ${pricePerBlock} PLUR per block)`
Utils.getStampDuration(amount, currentPrice, 5).toSeconds(),
)} (with price of ${currentPrice} PLUR per block)`
}
function getPrice(depth: number, amount: bigint): string {
@@ -106,15 +113,15 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen
}
setSubmitting(true)
const amount = BigInt(amountInput)
const depth = depthInput
const options: PostageBatchOptions = {
waitForUsable: false,
label: labelInput || undefined,
immutableFlag: true,
}
await beeApi.createPostageBatch(amount.toString(), depth, options)
await beeApi.buyStorage(
Size.fromGigabytes(buttonValue),
Duration.fromDays(sliderValue),
{ label: labelInput, immutableFlag: true },
undefined,
false,
RedundancyLevel.OFF,
)
await refresh()
onFinished()
} catch (e) {
@@ -127,8 +134,8 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen
function handleBatchSize(gigabytes: number) {
setButtonValue(gigabytes)
const capacity = Utils.getDepthForSize(Size.fromGigabytes(gigabytes))
setDepthInput(capacity)
const capacity = Utils.getDepthForSize(Size.fromGigabytes(gigabytes), false, RedundancyLevel.OFF)
validateDepthInput(String(capacity), setDepthError, (v: string) => setDepthInput(Number(v)))
}
return (
@@ -162,6 +169,7 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen
{getBatchValue(32)}
{getBatchValue(256)}
</Box>
{depthError && <Typography>{depthError}</Typography>}
</Box>
<Box mb={1}>
<Typography variant="h2">Data persistence</Typography>
@@ -183,11 +191,12 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen
<Grid container justifyContent="space-between">
<Typography>Corresponding TTL (Time to live)</Typography>
<Typography>{amountInput ? getTtl(amountInput) : '-'}</Typography>
<Typography>{amountInput ? getTtl(amountInput) : '-'}</Typography>
</Grid>
</Box>
<Box display="flex" justifyContent={'right'} mt={0.5}>
<Typography style={{ fontSize: '10px', color: 'rgba(0, 0, 0, 0.26)' }}>
Current price of 24000 PLUR per block
Current price of {currentPrice} PLUR per block
</Typography>
</Box>
</Box>
@@ -200,7 +209,7 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen
<Grid container justifyContent="space-between" alignItems="center">
<Grid>
<SwarmButton
disabled={submitting || !depthInput || !amountInput}
disabled={submitting || !depthInput || Boolean(depthError) || !amountInput}
onClick={submit}
iconType={Check}
loading={submitting}
+13 -9
View File
@@ -7,8 +7,9 @@ import ExpandableList from '../../components/ExpandableList'
import ExpandableListItem from '../../components/ExpandableListItem'
import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import StampExtensionModal from '../../components/StampExtensionModal'
import { Context } from '../../providers/Settings'
import StampExtensionModal, { StampExtensionType } from '../../components/StampExtensionModal'
import { Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings'
import { EnrichedPostageBatch } from '../../providers/Stamps'
import { secondsToTimeString } from '../../utils'
import { getHumanReadableFileSize } from '../../utils/file'
@@ -20,7 +21,8 @@ interface Props {
}
function StampsTable({ postageStamps }: Props): ReactElement | null {
const { beeApi } = useContext(Context)
const { beeApi } = useContext(SettingsContext)
const { status } = useContext(BeeContext)
if (!postageStamps || !beeApi) {
return null
@@ -37,8 +39,8 @@ function StampsTable({ postageStamps }: Props): ReactElement | null {
<ExpandableListItem label="Depth" value={String(stamp.depth)} />
<ExpandableListItem
label="Capacity"
value={`${getHumanReadableFileSize(2 ** stamp.depth * 4096 * stamp.usage)} / ${getHumanReadableFileSize(
2 ** stamp.depth * 4096,
value={`${getHumanReadableFileSize(stamp.size.toBytes() - stamp.remainingSize.toBytes())} / ${getHumanReadableFileSize(
stamp.size.toBytes(),
)}`}
/>
<ExpandableListItem label="Amount" value={parseInt(stamp.amount, 10).toLocaleString()} />
@@ -49,16 +51,18 @@ function StampsTable({ postageStamps }: Props): ReactElement | null {
<ExpandableListItem label="Purchase Block Number" value={stamp.blockNumber} />
<ExpandableListItemActions>
<StampExtensionModal
type="Topup"
type={StampExtensionType.Topup}
icon={<TimerFlashFill size="1rem" />}
bee={beeApi}
stamp={stamp.batchID}
stamp={stamp}
status={status.all}
/>
<StampExtensionModal
type="Dilute"
type={StampExtensionType.Dilute}
icon={<TimerFlashLine size="1rem" />}
bee={beeApi}
stamp={stamp.batchID}
stamp={stamp}
status={status.all}
/>
</ExpandableListItemActions>
</>