From cb6854eb68ffe3064a39a171bc1e23f628ee93bb Mon Sep 17 00:00:00 2001 From: Balint Ujvari Date: Wed, 25 Mar 2026 14:26:51 +0100 Subject: [PATCH 01/11] fix: swap error caused by invalid id and batchcount fix: import paths for src fix: react error for listitems --- .env.development | 5 +++- .gitignore | 1 + src/components/ExpandableListItem.tsx | 2 +- .../FileBrowserHeader/FileBrowserHeader.tsx | 3 +- .../PrivateKeyModal/PrivateKeyModal.tsx | 3 +- src/pages/filemanager/index.tsx | 30 +++++++++---------- src/pages/info/WalletInfoCard.tsx | 3 +- src/providers/File.tsx | 3 +- src/utils/chain.ts | 26 ++++++++++++++-- src/utils/rpc.ts | 4 +-- 10 files changed, 52 insertions(+), 28 deletions(-) diff --git a/.env.development b/.env.development index b729870d..e7c42ff1 100644 --- a/.env.development +++ b/.env.development @@ -1,3 +1,6 @@ PORT=3002 +VITE_BEE_DESKTOP_URL=http://localhost:3054 VITE_FORMBRICKS_ENV_ID= -VITE_FORMBRICKS_APP_URL= \ No newline at end of file +VITE_FORMBRICKS_APP_URL= +VITE_DEFAULT_RPC_URL= +VITE_BEE_DESKTOP_ENABLED= \ No newline at end of file diff --git a/.gitignore b/.gitignore index e675c10a..6e84d551 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +/**.log settings.json diff --git a/src/components/ExpandableListItem.tsx b/src/components/ExpandableListItem.tsx index ff051de5..5a2a297b 100644 --- a/src/components/ExpandableListItem.tsx +++ b/src/components/ExpandableListItem.tsx @@ -40,7 +40,7 @@ export default function ExpandableListItem({ label, value, tooltip }: Props): Re )} {value && ( - + {value} {tooltip && ( diff --git a/src/modules/filemanager/components/FileBrowser/FileBrowserHeader/FileBrowserHeader.tsx b/src/modules/filemanager/components/FileBrowser/FileBrowserHeader/FileBrowserHeader.tsx index c0b70931..73a57670 100644 --- a/src/modules/filemanager/components/FileBrowser/FileBrowserHeader/FileBrowserHeader.tsx +++ b/src/modules/filemanager/components/FileBrowser/FileBrowserHeader/FileBrowserHeader.tsx @@ -3,8 +3,7 @@ import DownIcon from 'remixicon-react/ArrowDownSLineIcon' import { BulkActionsResult } from '../../../hooks/useBulkActions' import { SortDir, SortKey } from '../../../hooks/useSorting' - -import { capitalizeFirstLetter } from '@/modules/filemanager/utils/common' +import { capitalizeFirstLetter } from '../../../utils/common' interface FileBrowserHeaderProps { isSearchMode: boolean diff --git a/src/modules/filemanager/components/PrivateKeyModal/PrivateKeyModal.tsx b/src/modules/filemanager/components/PrivateKeyModal/PrivateKeyModal.tsx index a57d82b9..d896316c 100644 --- a/src/modules/filemanager/components/PrivateKeyModal/PrivateKeyModal.tsx +++ b/src/modules/filemanager/components/PrivateKeyModal/PrivateKeyModal.tsx @@ -3,6 +3,7 @@ import { ReactElement, useState } from 'react' import CheckDoubleLineIcon from 'remixicon-react/CheckDoubleLineIcon' import ClipboardIcon from 'remixicon-react/FileCopyLineIcon' +import { uuidV4 } from '../../../../utils' import { TOOLTIPS } from '../../constants/tooltips' import { getSigner, setSignerPk } from '../../utils/common' import { Button } from '../Button/Button' @@ -10,8 +11,6 @@ import { Tooltip } from '../Tooltip/Tooltip' import './PrivateKeyModal.scss' -import { uuidV4 } from '@/utils' - type Props = { onSaved: () => void } const generateNewPrivateKey = (): string => { diff --git a/src/pages/filemanager/index.tsx b/src/pages/filemanager/index.tsx index 699ec034..106c1ca5 100644 --- a/src/pages/filemanager/index.tsx +++ b/src/pages/filemanager/index.tsx @@ -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 (
diff --git a/src/pages/info/WalletInfoCard.tsx b/src/pages/info/WalletInfoCard.tsx index c8c8ca35..5181dd3d 100644 --- a/src/pages/info/WalletInfoCard.tsx +++ b/src/pages/info/WalletInfoCard.tsx @@ -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 ( ): Promise> { + const results = await super._send(payload) + const payloads = Array.isArray(payload) ? payload : [payload] + + return results.map((result, i) => ({ ...result, id: payloads[i]?.id ?? result.id })) + } +} + export function newGnosisProvider(url: string): JsonRpcProvider { - return new JsonRpcProvider(url, GnosisNetwork, { staticNetwork: true }) + return new FixedIdJsonRpcProvider(url, GnosisNetwork, { staticNetwork: true, batchMaxCount: 1 }) +} + +/** + * Provider for RPC validation only — no staticNetwork so getNetwork() actually + * calls eth_chainId, but still uses FixedIdJsonRpcProvider to handle endpoints + * that return a fixed/wrong id in their responses. + */ +export function newGnosisProviderForValidation(url: string): JsonRpcProvider { + return new FixedIdJsonRpcProvider(url, undefined, { batchMaxCount: 1 }) } diff --git a/src/utils/rpc.ts b/src/utils/rpc.ts index 67ee665e..eb0984c6 100644 --- a/src/utils/rpc.ts +++ b/src/utils/rpc.ts @@ -3,10 +3,10 @@ import { debounce } from '@mui/material' import { Contract, JsonRpcProvider, TransactionReceipt, TransactionResponse, Wallet } from 'ethers' import { BZZ_TOKEN_ADDRESS, bzzABI } from './bzzAbi' -import { ethAddressString, newGnosisProvider } from './chain' +import { ethAddressString, newGnosisProvider, newGnosisProviderForValidation } from './chain' async function getNetworkChainId(url: string): Promise { - const provider = newGnosisProvider(url) + const provider = newGnosisProviderForValidation(url) const network = await provider.getNetwork() return network.chainId From bb93d5c26fa5414c6423b87a3992e0f2e410e515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferenc=20S=C3=A1rai?= Date: Tue, 31 Mar 2026 17:54:14 +0200 Subject: [PATCH 02/11] fix: enhance creation messages for admin drive and user drives spdv-942 (#238) * fix: enhance creation messages for admin drive and user drives spdv-942 * fix: update creation message to indicate longer processing time spdv-942 --- .../components/AdminStatusBar/AdminStatusBar.tsx | 8 +++++++- .../filemanager/components/ConfirmModal/ConfirmModal.tsx | 2 +- src/modules/filemanager/components/Sidebar/Sidebar.tsx | 6 +++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/modules/filemanager/components/AdminStatusBar/AdminStatusBar.tsx b/src/modules/filemanager/components/AdminStatusBar/AdminStatusBar.tsx index 953c2733..39d1ba0a 100644 --- a/src/modules/filemanager/components/AdminStatusBar/AdminStatusBar.tsx +++ b/src/modules/filemanager/components/AdminStatusBar/AdminStatusBar.tsx @@ -179,7 +179,13 @@ export function AdminStatusBar({ const isBusy = loading || isUpgrading || isCreationInProgress const blurCls = isBusy ? ' is-loading' : '' const statusVerb = isCreationInProgress ? 'Creating' : 'Loading' - const statusText = statusVerb + ' admin drive, please do not reload' + const statusText = ( + <> + {statusVerb} admin drive — please do not reload the page. +
+ This may take a few minutes. + + ) const renderModalsAndOverlays = () => { return ( diff --git a/src/modules/filemanager/components/ConfirmModal/ConfirmModal.tsx b/src/modules/filemanager/components/ConfirmModal/ConfirmModal.tsx index f849cf69..f0f7f886 100644 --- a/src/modules/filemanager/components/ConfirmModal/ConfirmModal.tsx +++ b/src/modules/filemanager/components/ConfirmModal/ConfirmModal.tsx @@ -15,7 +15,7 @@ interface ConfirmModalProps { onCancel?: () => void showFooter?: boolean isProgress?: boolean - spinnerMessage?: string + spinnerMessage?: React.ReactNode showMinimize?: boolean onMinimize?: () => void background?: boolean diff --git a/src/modules/filemanager/components/Sidebar/Sidebar.tsx b/src/modules/filemanager/components/Sidebar/Sidebar.tsx index eeae854f..55a25389 100644 --- a/src/modules/filemanager/components/Sidebar/Sidebar.tsx +++ b/src/modules/filemanager/components/Sidebar/Sidebar.tsx @@ -305,7 +305,11 @@ export function Sidebar({ setErrorMessage, loading }: SidebarProps): ReactElemen
{isDriveCreationInProgress && ( -
Creating drive, please do not reload
+
+ Creating drive — please do not reload the page. +
+ This may take a few minutes. +
)} ) From c08bf8a40bda76ccaff6056dfbd946a6bed2c2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1lint=20Ujv=C3=A1ri?= <58116288+bosi95@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:36:37 +0200 Subject: [PATCH 03/11] fix: identity and wallet creation (#240) fix: asset preview types fix: fm search unicode text fix: feed identity and stamp usage --- src/components/SwarmSelect.tsx | 1 + src/components/SwarmTextInput.tsx | 4 +- .../filemanager/hooks/useFileFiltering.ts | 10 +- .../filemanager/utils/GetIconElement.tsx | 2 +- src/modules/filemanager/utils/download.ts | 2 +- src/modules/filemanager/utils/view.ts | 114 ----------------- src/pages/account/feeds/AccountFeeds.tsx | 2 +- src/pages/feeds/CreateNewFeed.tsx | 52 ++++++-- src/pages/feeds/UpdateFeed.tsx | 27 ++-- src/pages/feeds/index.tsx | 2 +- src/pages/files/AssetPreview.tsx | 28 +++-- src/pages/files/Share.tsx | 6 +- src/utils/file.ts | 116 ++++++++++++++++++ src/utils/identity.ts | 4 +- 14 files changed, 213 insertions(+), 157 deletions(-) delete mode 100644 src/modules/filemanager/utils/view.ts diff --git a/src/components/SwarmSelect.tsx b/src/components/SwarmSelect.tsx index 04243e69..2d77f1cd 100644 --- a/src/components/SwarmSelect.tsx +++ b/src/components/SwarmSelect.tsx @@ -75,6 +75,7 @@ export function SwarmSelect({ value={value} className={classes.select} displayEmpty + onChange={onChange} renderValue={(value: unknown) => (value ? renderValue(value) : placeholder)} MenuProps={{ MenuListProps: { disablePadding: true }, PaperProps: { square: true } }} > diff --git a/src/components/SwarmTextInput.tsx b/src/components/SwarmTextInput.tsx index 3d6e6a7e..25e5ad87 100644 --- a/src/components/SwarmTextInput.tsx +++ b/src/components/SwarmTextInput.tsx @@ -57,7 +57,7 @@ export function SwarmTextInput({ variant="filled" className={classes.field} defaultValue={defaultValue || ''} - InputProps={{ disableUnderline: true }} + slotProps={{ input: { disableUnderline: true } }} placeholder={placeholder} /> ) @@ -73,7 +73,7 @@ export function SwarmTextInput({ className={classes.field} defaultValue={defaultValue || ''} onChange={onChange} - InputProps={{ disableUnderline: true }} + slotProps={{ input: { disableUnderline: true } }} placeholder={placeholder} /> ) diff --git a/src/modules/filemanager/hooks/useFileFiltering.ts b/src/modules/filemanager/hooks/useFileFiltering.ts index 8fb92b39..ebd2b04f 100644 --- a/src/modules/filemanager/hooks/useFileFiltering.ts +++ b/src/modules/filemanager/hooks/useFileFiltering.ts @@ -26,7 +26,7 @@ interface UseFileFilteringReturn { export function useFileFiltering(props: UseFileFilteringProps): UseFileFilteringReturn { const { files, currentDrive, view, isSearchMode, query, scope, includeActive, includeTrashed } = props - const q = query.trim().toLowerCase() + const q = query.trim().toLowerCase().normalize('NFC') const statusIncluded = useCallback( (fi: FileInfo): boolean => { @@ -44,9 +44,11 @@ export function useFileFiltering(props: UseFileFilteringProps): UseFileFiltering const matchesQuery = useCallback( (fi: FileInfo): boolean => { if (!q) return true - const name = fi.name.toLowerCase() - const mime = (fi.customMetadata?.mime || '').toLowerCase() - const topic = String(fi.topic ?? '').toLowerCase() + const name = fi.name.toLowerCase().normalize('NFC') + const mime = (fi.customMetadata?.mime || '').toLowerCase().normalize('NFC') + const topic = String(fi.topic ?? '') + .toLowerCase() + .normalize('NFC') return name.includes(q) || mime.includes(q) || topic.includes(q) }, diff --git a/src/modules/filemanager/utils/GetIconElement.tsx b/src/modules/filemanager/utils/GetIconElement.tsx index 3e93fe77..68aace70 100644 --- a/src/modules/filemanager/utils/GetIconElement.tsx +++ b/src/modules/filemanager/utils/GetIconElement.tsx @@ -2,7 +2,7 @@ import { ReactElement } from 'react' import FileIcon from 'remixicon-react/FileTextLineIcon' import ImageIcon from 'remixicon-react/Image2LineIcon' -import { guessMime } from './view' +import { guessMime } from '../../../utils/file' interface ContextMenuProps { name: string diff --git a/src/modules/filemanager/utils/download.ts b/src/modules/filemanager/utils/download.ts index 151d9262..17dcba8c 100644 --- a/src/modules/filemanager/utils/download.ts +++ b/src/modules/filemanager/utils/download.ts @@ -1,10 +1,10 @@ import { FileInfo, FileManager } from '@solarpunkltd/file-manager-lib' +import { guessMime, VIEWERS } from '../../../utils/file' import { DownloadProgress, DownloadState } from '../constants/transfers' import { AbortManager } from './abortManager' import { isDirectoryPickerSupported, isPickerSupported } from './fileOperations' -import { guessMime, VIEWERS } from './view' const DefaultDownloadFolder = 'downloads' diff --git a/src/modules/filemanager/utils/view.ts b/src/modules/filemanager/utils/view.ts deleted file mode 100644 index 163cec2b..00000000 --- a/src/modules/filemanager/utils/view.ts +++ /dev/null @@ -1,114 +0,0 @@ -const EXT_TO_MIME: Record = { - mp4: 'video/mp4', - webm: 'video/webm', - ogv: 'video/ogg', - mp3: 'audio/mpeg', - m4a: 'audio/mp4', - aac: 'audio/aac', - wav: 'audio/wav', - ogg: 'audio/ogg', - png: 'image/png', - jpg: 'image/jpeg', - jpeg: 'image/jpeg', - gif: 'image/gif', - webp: 'image/webp', - avif: 'image/avif', - svg: 'image/svg+xml', - pdf: 'application/pdf', - txt: 'text/plain', - md: 'text/markdown', - json: 'application/json', - csv: 'text/csv', - html: 'text/html', - htm: 'text/html', -} - -export function getExtensionFromName(name: string): string { - const ext = name.split('.').pop()?.toLowerCase() || '' - const hasExtension = name.includes('.') && ext && ext !== name - - return hasExtension ? ext : '' -} - -export function guessMime(name: string, mtdt?: Record | undefined): { mime: string; ext: string } { - const md = mtdt?.mimeType || mtdt?.mime || mtdt?.['content-type'] - const ext = getExtensionFromName(name) - - if (md) return { mime: md, ext } - - const mime = EXT_TO_MIME[ext] || 'application/octet-stream' - - return { mime, ext } -} - -export type Viewer = { - name: string - test: (mime: string) => boolean - render: (win: Window, url: string, mime: string, name: string) => void -} - -const VIDEO_HTML = (u: string, title: string) => - `${title} - - ` - -const AUDIO_HTML = (u: string, title: string) => - `${title} - - ` - -const IMAGE_HTML = (u: string, title: string) => - `${title} - - ` - -export const VIEWERS: Viewer[] = [ - { - name: 'video', - test: m => m.startsWith('video/'), - render: (w, url, mime, name) => { - w.document.write(VIDEO_HTML(url, name)) - w.document.title = name - }, - }, - { - name: 'audio', - test: m => m.startsWith('audio/'), - render: (w, url, mime, name) => { - w.document.write(AUDIO_HTML(url, name)) - w.document.title = name - }, - }, - { - name: 'image', - test: m => m.startsWith('image/'), - render: (w, url, mime, name) => { - w.document.write(IMAGE_HTML(url, name)) - w.document.title = name - }, - }, - { - name: 'pdf', - test: m => m === 'application/pdf', - render: (w, url, mime, name) => { - w.document.title = name - w.location.href = url - }, - }, - { - name: 'html', - test: m => m === 'text/html', - render: (w, url, mime, name) => { - w.document.title = name - w.location.href = url - }, - }, - { - name: 'text-like', - test: m => m.startsWith('text/') || m === 'application/json' || m === 'text/markdown', - render: (w, url, mime, name) => { - w.document.title = name - w.location.href = url - }, - }, -] diff --git a/src/pages/account/feeds/AccountFeeds.tsx b/src/pages/account/feeds/AccountFeeds.tsx index 6a98f384..acdc07a8 100644 --- a/src/pages/account/feeds/AccountFeeds.tsx +++ b/src/pages/account/feeds/AccountFeeds.tsx @@ -104,7 +104,7 @@ export function AccountFeeds(): ReactElement { {x.feedHash && } - viewFeed(x.uuid)} iconType={Info}> + viewFeed(x.uuid)} iconType={Info} disabled={Boolean(!x.feedHash)}> View Feed Page onShowExport(x)} iconType={Download}> diff --git a/src/pages/feeds/CreateNewFeed.tsx b/src/pages/feeds/CreateNewFeed.tsx index 7321bbc0..5a78bf37 100644 --- a/src/pages/feeds/CreateNewFeed.tsx +++ b/src/pages/feeds/CreateNewFeed.tsx @@ -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.PrivateKey) + const [loading, setLoading] = useState(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(Error during wallet generation or postage stamp retrieval!, { variant: 'error' }) + setLoading(false) + + return + } if (!stamps || !stamps.length) { - enqueueSnackbar(No stamp available, { variant: 'error' }) + enqueueSnackbar(No usable stamp available, { 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(Error identity creation!, { variant: 'error' }) + } finally { + setLoading(false) + } } function cancel() { navigate(-1) } + function onIdentityTypeChange(event: SelectEvent) { + const type = event.target.value as IdentityType + setIdentityType(type) + } + return (
Create new feed @@ -102,10 +129,13 @@ export default function CreateNewFeed(): ReactElement { {values.type === IdentityType.V3 && } diff --git a/src/pages/feeds/UpdateFeed.tsx b/src/pages/feeds/UpdateFeed.tsx index dd9a97af..b75718cc 100644 --- a/src/pages/feeds/UpdateFeed.tsx +++ b/src/pages/feeds/UpdateFeed.tsx @@ -26,8 +26,8 @@ export default function UpdateFeed(): ReactElement { const { status } = useContext(BeeContext) const { hash } = useParams() - const [selectedStamp, setSelectedStamp] = useState(null) - const [selectedIdentity, setSelectedIdentity] = useState(null) + const [selectedStamp, setSelectedStamp] = useState(stamps ? stamps[0]?.batchID.toHex() : null) + const [selectedIdentity, setSelectedIdentity] = useState(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 { Update feed - ({ value: x.uuid, label: `${x.name} Website` }))} - onChange={onFeedChange} - label="Feed" - /> + {identities && identities.length ? ( + ({ value: x.uuid, label: `${x.name} Website` }))} + onChange={onFeedChange} + label="Feed" + /> + ) : ( + You need to create an identiy first to be able to update its feed. + )} - {stamps ? ( + {stamps && stamps.length ? ( ({ 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" /> diff --git a/src/pages/feeds/index.tsx b/src/pages/feeds/index.tsx index bb6d548b..6cd102cd 100644 --- a/src/pages/feeds/index.tsx +++ b/src/pages/feeds/index.tsx @@ -104,7 +104,7 @@ export default function Feeds(): ReactElement { {x.feedHash && } - viewFeed(x.uuid)} iconType={Info}> + viewFeed(x.uuid)} iconType={Info} disabled={Boolean(!x.feedHash)}> View Feed Page onShowExport(x)} iconType={Download}> diff --git a/src/pages/files/AssetPreview.tsx b/src/pages/files/AssetPreview.tsx index 797556db..8608d3c6 100644 --- a/src/pages/files/AssetPreview.tsx +++ b/src/pages/files/AssetPreview.tsx @@ -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 } - if (metadata?.isAudio) { + if (metadata?.isAudio || isAudioType) { return } - if (metadata?.isImage) { + if (metadata?.isImage || isImageType) { return } @@ -42,18 +46,26 @@ const getPreviewElement = (previewUri?: string, metadata?: Metadata) => { return } /> } -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 ( diff --git a/src/pages/files/Share.tsx b/src/pages/files/Share.tsx index a391cba7..e027a683 100644 --- a/src/pages/files/Share.tsx +++ b/src/pages/files/Share.tsx @@ -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)), diff --git a/src/utils/file.ts b/src/utils/file.ts index 1f504d54..40f5ea97 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -137,3 +137,119 @@ export function packageFile(file: FilePath, pathOverwrite?: string): FilePath { bytes: file.bytes, } } + +export function getExtensionFromName(name: string): string { + const ext = name.split('.').pop()?.toLowerCase() || '' + const hasExtension = name.includes('.') && ext && ext !== name + + return hasExtension ? ext : '' +} + +const EXT_TO_MIME: Record = { + mp4: 'video/mp4', + webm: 'video/webm', + ogv: 'video/ogg', + mp3: 'audio/mpeg', + m4a: 'audio/mp4', + aac: 'audio/aac', + wav: 'audio/wav', + ogg: 'audio/ogg', + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif', + webp: 'image/webp', + avif: 'image/avif', + svg: 'image/svg+xml', + pdf: 'application/pdf', + txt: 'text/plain', + md: 'text/markdown', + json: 'application/json', + csv: 'text/csv', + html: 'text/html', + htm: 'text/html', + ico: 'image/vnd.microsoft.icon', +} + +export function guessMime(name: string, mtdt?: Record | undefined): { mime: string; ext: string } { + const md = mtdt?.mimeType || mtdt?.mime || mtdt?.['content-type'] + const ext = getExtensionFromName(name) + + if (md) return { mime: md, ext } + + const mime = EXT_TO_MIME[ext] || 'application/octet-stream' + + return { mime, ext } +} + +export type Viewer = { + name: string + test: (mime: string) => boolean + render: (win: Window, url: string, mime: string, name: string) => void +} + +const VIDEO_HTML = (u: string, title: string) => + `${title} + + ` + +const AUDIO_HTML = (u: string, title: string) => + `${title} + + ` + +const IMAGE_HTML = (u: string, title: string) => + `${title} + + ` + +export const VIEWERS: Viewer[] = [ + { + name: 'video', + test: m => m.startsWith('video/'), + render: (w, url, mime, name) => { + w.document.write(VIDEO_HTML(url, name)) + w.document.title = name + }, + }, + { + name: 'audio', + test: m => m.startsWith('audio/'), + render: (w, url, mime, name) => { + w.document.write(AUDIO_HTML(url, name)) + w.document.title = name + }, + }, + { + name: 'image', + test: m => m.startsWith('image/'), + render: (w, url, mime, name) => { + w.document.write(IMAGE_HTML(url, name)) + w.document.title = name + }, + }, + { + name: 'pdf', + test: m => m === 'application/pdf', + render: (w, url, mime, name) => { + w.document.title = name + w.location.href = url + }, + }, + { + name: 'html', + test: m => m === 'text/html', + render: (w, url, mime, name) => { + w.document.title = name + w.location.href = url + }, + }, + { + name: 'text-like', + test: m => m.startsWith('text/') || m === 'application/json' || m === 'text/markdown', + render: (w, url, mime, name) => { + w.document.title = name + w.location.href = url + }, + }, +] diff --git a/src/utils/identity.ts b/src/utils/identity.ts index 1a34ef3c..d6bd9c05 100644 --- a/src/utils/identity.ts +++ b/src/utils/identity.ts @@ -1,4 +1,4 @@ -import { BatchId, Bee, NULL_TOPIC, PrivateKey, Reference } from '@ethersphere/bee-js' +import { BatchId, Bee, Bytes, NULL_TOPIC, PrivateKey, Reference } from '@ethersphere/bee-js' import { randomBytes, Wallet } from 'ethers' import { Identity, IdentityType } from '../providers/Feeds' @@ -7,7 +7,7 @@ import { LocalStorageKeys } from './localStorage' import { uuidV4, waitUntilStampUsable } from '.' export function generateWallet(): Wallet { - const privateKey = randomBytes(PrivateKey.LENGTH).toString() + const privateKey = new Bytes(randomBytes(PrivateKey.LENGTH)).toString() return new Wallet(privateKey) } From d65da143d2200db653fe7a80a7891dacf4c2937e Mon Sep 17 00:00:00 2001 From: rolandlor <33499567+rolandlor@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:34:10 +0200 Subject: [PATCH 04/11] fix: ui display changes spdv-1018 (#239) fix: ui layout changes --- src/components/ExpandableListItemInput.tsx | 128 +++++++++++---------- src/components/ExpandableListItemKey.tsx | 12 +- src/components/ExpandableListItemLink.tsx | 10 +- src/components/SideBar.tsx | 2 +- src/components/StampExtensionModal.tsx | 19 ++- src/components/WithdrawDepositModal.tsx | 12 +- 6 files changed, 110 insertions(+), 73 deletions(-) diff --git a/src/components/ExpandableListItemInput.tsx b/src/components/ExpandableListItemInput.tsx index 2c3a4c42..5a94325c 100644 --- a/src/components/ExpandableListItemInput.tsx +++ b/src/components/ExpandableListItemInput.tsx @@ -16,11 +16,17 @@ const useStyles = makeStyles()(theme => ({ header: { backgroundColor: theme.palette.background.paper, marginBottom: theme.spacing(0.25), - borderLeft: `${theme.spacing(0.25)}px solid rgba(0,0,0,0)`, + borderLeft: `${theme.spacing(0.25)} solid rgba(0,0,0,0)`, wordBreak: 'break-word', + '&:hover': { + backgroundColor: theme.palette.background.paper, + }, + '&:focus-within': { + backgroundColor: theme.palette.background.paper, + }, }, headerOpen: { - borderLeft: `${theme.spacing(0.25)}px solid ${theme.palette.primary.main}`, + borderLeft: `${theme.spacing(0.25)} solid ${theme.palette.primary.main}`, }, copyValue: { cursor: 'pointer', @@ -95,35 +101,35 @@ export default function ExpandableListItemInput({ } return ( - - - - {label && ( - - - {label} - - - )} - - {!open && value && ( - - {value} - - )} - {!expandedOnly && !locked && ( - - {open ? : } - + <> + + + + {label && ( + + + {label} + + )} + + {!open && value && ( + + {value} + + )} + {!expandedOnly && !locked && ( + + {open ? : } + + )} + - - - + {helperText && {helperText}} - - - { - onConfirm?.(inputValue.trim()) - }} - > - {confirmLabel || 'Save'} - - setInputValue(value || '')} - cancel - > - Cancel - - - - - - - + + + + + + + { + onConfirm?.(inputValue.trim()) + }} + > + {confirmLabel || 'Save'} + + setInputValue(value || '')} + cancel + > + Cancel + + + + + ) } diff --git a/src/components/ExpandableListItemKey.tsx b/src/components/ExpandableListItemKey.tsx index feeba054..72449cef 100644 --- a/src/components/ExpandableListItemKey.tsx +++ b/src/components/ExpandableListItemKey.tsx @@ -10,11 +10,17 @@ const useStyles = makeStyles()(theme => ({ header: { backgroundColor: theme.palette.background.paper, marginBottom: theme.spacing(0.25), - borderLeft: `${theme.spacing(0.25)}px solid rgba(0,0,0,0)`, + borderLeft: `${theme.spacing(0.25)} solid rgba(0,0,0,0)`, wordBreak: 'break-word', + '&:hover': { + backgroundColor: theme.palette.background.paper, + }, + '&:focus-within': { + backgroundColor: theme.palette.background.paper, + }, }, headerOpen: { - borderLeft: `${theme.spacing(0.25)}px solid ${theme.palette.primary.main}`, + borderLeft: `${theme.spacing(0.25)} solid ${theme.palette.primary.main}`, }, copyValue: { cursor: 'pointer', @@ -69,7 +75,7 @@ export default function ExpandableListItemKey({ label, value, expanded }: Props) return ( - + {label && ( diff --git a/src/components/ExpandableListItemLink.tsx b/src/components/ExpandableListItemLink.tsx index 72a352b6..483b481f 100644 --- a/src/components/ExpandableListItemLink.tsx +++ b/src/components/ExpandableListItemLink.tsx @@ -10,11 +10,17 @@ const useStyles = makeStyles()(theme => ({ header: { backgroundColor: theme.palette.background.paper, marginBottom: theme.spacing(0.25), - borderLeft: `${theme.spacing(0.25)}px solid rgba(0,0,0,0)`, + borderLeft: `${theme.spacing(0.25)} solid rgba(0,0,0,0)`, wordBreak: 'break-word', + '&:hover': { + backgroundColor: theme.palette.background.paper, + }, + '&:focus-within': { + backgroundColor: theme.palette.background.paper, + }, }, headerOpen: { - borderLeft: `${theme.spacing(0.25)}px solid ${theme.palette.primary.main}`, + borderLeft: `${theme.spacing(0.25)} solid ${theme.palette.primary.main}`, }, openLinkIcon: { cursor: 'pointer', diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index e714139c..b29d6886 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -139,7 +139,7 @@ export default function SideBar(): ReactElement { label: 'File Manager', path: ROUTES.FILEMANAGER, icon: FileManagerIcon, - pathMatcherSubstring: '/filemanager/', + pathMatcherSubstring: '/filemanager', }, { label: 'Account', diff --git a/src/components/StampExtensionModal.tsx b/src/components/StampExtensionModal.tsx index 7c7981ca..1da519f9 100644 --- a/src/components/StampExtensionModal.tsx +++ b/src/components/StampExtensionModal.tsx @@ -8,6 +8,22 @@ import DialogTitle from '@mui/material/DialogTitle' import Input from '@mui/material/Input' import { useSnackbar } from 'notistack' import React, { ReactElement, ReactNode, useState } from 'react' +import { makeStyles } from 'tss-react/mui' + +const useStyles = makeStyles()(() => ({ + button: { + color: '#333333', + backgroundColor: 'white', + '&:hover': { + backgroundColor: '#dd7700', + color: 'white', + '@media (hover: none)': { + backgroundColor: '#dd7700', + color: 'white', + }, + }, + }, +})) interface Props { type: 'Topup' | 'Dilute' @@ -17,6 +33,7 @@ interface Props { } export default function StampExtensionModal({ type, icon, bee, stamp }: Props): ReactElement { + const { classes } = useStyles() const [open, setOpen] = useState(false) const [amount, setAmount] = useState('') const { enqueueSnackbar } = useSnackbar() @@ -57,7 +74,7 @@ export default function StampExtensionModal({ type, icon, bee, stamp }: Props): return ( - diff --git a/src/components/WithdrawDepositModal.tsx b/src/components/WithdrawDepositModal.tsx index 33d6c45a..59dc01a6 100644 --- a/src/components/WithdrawDepositModal.tsx +++ b/src/components/WithdrawDepositModal.tsx @@ -24,14 +24,14 @@ const useStyles = makeStyles()(theme => ({ }, }, buttonSelected: { - color: 'white', - backgroundColor: theme.palette.primary.main, + color: theme.palette.secondary.main, + backgroundColor: 'white', '&:hover': { - color: theme.palette.secondary.main, - backgroundColor: 'white', + color: 'white', + backgroundColor: theme.palette.primary.main, '@media (hover: none)': { - color: 'white', - backgroundColor: theme.palette.primary.main, + color: theme.palette.secondary.main, + backgroundColor: 'white', }, }, }, From c890f7c1e8e4d21f8d252b3e1a9c783982459adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1lint=20Ujv=C3=A1ri?= <58116288+bosi95@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:04:43 +0200 Subject: [PATCH 05/11] fix: stamp buy and dilute (#242) fix: vite polyfill warning for stream refactor: stamp depth and amount validation --- src/components/StampExtensionModal.tsx | 103 ++++++++++++++---- src/constants.ts | 2 + src/pages/files/Upload.tsx | 2 +- .../stamps/PostageStampAdvancedCreation.tsx | 35 ++---- .../stamps/PostageStampStandardCreation.tsx | 15 ++- src/pages/stamps/StampsTable.tsx | 18 +-- src/utils/identity.ts | 3 +- src/utils/index.ts | 35 +----- src/utils/stamp.ts | 63 +++++++++++ vite.config.mts | 2 +- 10 files changed, 181 insertions(+), 97 deletions(-) create mode 100644 src/utils/stamp.ts diff --git a/src/components/StampExtensionModal.tsx b/src/components/StampExtensionModal.tsx index 1da519f9..d72fd3a4 100644 --- a/src/components/StampExtensionModal.tsx +++ b/src/components/StampExtensionModal.tsx @@ -1,4 +1,4 @@ -import { BatchId, Bee } from '@ethersphere/bee-js' +import { Bee, PostageBatch } from '@ethersphere/bee-js' import { Box } from '@mui/material' import Button from '@mui/material/Button' import Dialog from '@mui/material/Dialog' @@ -10,34 +10,46 @@ import { useSnackbar } from 'notistack' import React, { ReactElement, ReactNode, useState } from 'react' import { makeStyles } from 'tss-react/mui' -const useStyles = makeStyles()(() => ({ - button: { - color: '#333333', - backgroundColor: 'white', +import { CheckState } from '../providers/Bee' + +const useStyles = makeStyles()(theme => ({ + buttonSelected: { + color: 'white', + backgroundColor: theme.palette.primary.main, '&:hover': { - backgroundColor: '#dd7700', - color: 'white', + color: theme.palette.secondary.main, + backgroundColor: 'white', '@media (hover: none)': { - backgroundColor: '#dd7700', color: 'white', + backgroundColor: theme.palette.primary.main, }, }, }, + buttonUnselected: { + color: '#dd7700', + backgroundColor: 'white', + }, })) +export enum StampExtensionType { + Topup = 'Topup', + Dilute = 'Dilute', +} + interface Props { - type: 'Topup' | 'Dilute' + type: StampExtensionType icon: ReactNode bee: Bee - stamp: BatchId + stamp: PostageBatch + status: CheckState } -export default function StampExtensionModal({ type, icon, bee, stamp }: Props): ReactElement { +export default function StampExtensionModal({ type, icon, bee, stamp, status }: Props): ReactElement { const { classes } = useStyles() - const [open, setOpen] = useState(false) - const [amount, setAmount] = useState('') + const [open, setOpen] = useState(false) + const [amount, setAmount] = useState('') const { enqueueSnackbar } = useSnackbar() - const label = `${type} ${stamp.toHex().substring(0, 8)}` + const label = `${type} ${stamp.batchID.toHex().substring(0, 8)}` const handleClickOpen = (e: React.MouseEvent) => { setOpen(true) @@ -49,23 +61,65 @@ export default function StampExtensionModal({ type, icon, bee, stamp }: Props): } const handleAction = async () => { - if (type === 'Topup') { + if (status !== CheckState.OK) { + enqueueSnackbar(`Node connection status is not ${CheckState.OK}: ${status}`, { variant: 'error' }) + + return + } + + if (type === StampExtensionType.Topup) { + const isAmountInvalid = BigInt(amount) <= BigInt(0) + + if (isAmountInvalid) { + enqueueSnackbar(`Invalid amount: ${amount}, it must be greate than 0`, { variant: 'error' }) + + return + } + try { - await bee.topUpBatch(stamp, amount) + await bee.topUpBatch(stamp.batchID, amount) enqueueSnackbar(`Successfully topped up stamp, your changes will appear soon`, { variant: 'success' }) } catch (error) { enqueueSnackbar(`Failed to topup stamp: ${error || 'Unknown reason'}`, { variant: 'error' }) } + + return } - if (type === 'Dilute') { + if (type === StampExtensionType.Dilute) { + const newDepth = parseInt(amount, 10) + const ttlDays = stamp.duration.toDays() + const currentDepth = stamp.depth + const maxHalvings = Math.floor(Math.log2(ttlDays)) + currentDepth + const isDepthInvalid = newDepth > maxHalvings || newDepth <= currentDepth + + if (isDepthInvalid) { + enqueueSnackbar(`Invalid depth: ${newDepth} (${currentDepth} < new depth < ${maxHalvings})`, { + variant: 'error', + }) + + return + } + + if (ttlDays <= 2) { + enqueueSnackbar(`TTL: ${ttlDays} <= 2 days, cannot dilute stamp (min. TTL is 1 day)`, { + variant: 'warning', + }) + + return + } + try { - await bee.diluteBatch(stamp, parseInt(amount, 10)) + await bee.diluteBatch(stamp.batchID, newDepth) enqueueSnackbar(`Successfully diluted stamp, your changes will appear soon`, { variant: 'success' }) } catch (error) { enqueueSnackbar(`Failed to dilute stamp: ${error || 'Unknown reason'}`, { variant: 'error' }) } + + return } + + enqueueSnackbar(`Failed to extend stamp, unknown operation: ${type}`, { variant: 'error' }) } const handleChange = (event: React.ChangeEvent) => { @@ -74,7 +128,7 @@ export default function StampExtensionModal({ type, icon, bee, stamp }: Props): return ( - @@ -85,7 +139,7 @@ export default function StampExtensionModal({ type, icon, bee, stamp }: Props): margin="dense" id="name" type="text" - placeholder={type === 'Topup' ? 'Amount to add' : 'New depth to dilute'} + placeholder={type === StampExtensionType.Topup ? 'Amount to add' : 'New depth to dilute'} fullWidth value={amount} onChange={handleChange} @@ -95,7 +149,14 @@ export default function StampExtensionModal({ type, icon, bee, stamp }: Props): - diff --git a/src/constants.ts b/src/constants.ts index 2a8faacd..1afe8b8c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,3 +12,5 @@ export const BEE_DESKTOP_LATEST_RELEASE_PAGE_API = 'https://api.github.com/repos/ethersphere/bee-desktop/releases/latest' export const DEFAULT_BEE_API_HOST = 'http://localhost:1633' export const DEFAULT_RPC_URL = 'https://xdai.fairdatasociety.org' +export const MIN_STAMP_DEPTH = 17 +export const MAX_STAMP_DEPTH = 255 diff --git a/src/pages/files/Upload.tsx b/src/pages/files/Upload.tsx index 940ecb94..0879b90d 100644 --- a/src/pages/files/Upload.tsx +++ b/src/pages/files/Upload.tsx @@ -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' diff --git a/src/pages/stamps/PostageStampAdvancedCreation.tsx b/src/pages/stamps/PostageStampAdvancedCreation.tsx index 9ea087d0..4dc68428 100644 --- a/src/pages/stamps/PostageStampAdvancedCreation.tsx +++ b/src/pages/stamps/PostageStampAdvancedCreation.tsx @@ -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,33 +149,10 @@ 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 '-' } @@ -217,7 +196,11 @@ export function PostageStampAdvancedCreation({ onFinished }: Props): ReactElemen - validateDepthInput(event.target.value)} /> + validateDepthInput(event.target.value, setDepthError, setDepthInput)} + /> Corresponding file size diff --git a/src/pages/stamps/PostageStampStandardCreation.tsx b/src/pages/stamps/PostageStampStandardCreation.tsx index 08f12ac4..bc53cf76 100644 --- a/src/pages/stamps/PostageStampStandardCreation.tsx +++ b/src/pages/stamps/PostageStampStandardCreation.tsx @@ -12,6 +12,7 @@ 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 @@ -51,9 +52,10 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen const [depthInput, setDepthInput] = useState(Utils.getDepthForSize(Size.fromGigabytes(4))) const [amountInput, setAmountInput] = useState(Utils.getAmountForDuration(Duration.fromDays(30), 26500, 5)) - const [labelInput, setLabelInput] = useState('') - const [submitting, setSubmitting] = useState(false) - const [buttonValue, setButtonValue] = useState(4) + const [labelInput, setLabelInput] = useState('') + const [submitting, setSubmitting] = useState(false) + const [buttonValue, setButtonValue] = useState(4) + const [depthError, setDepthError] = useState('') const getBatchValue = (value: number) => { return ( @@ -126,9 +128,9 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen } function handleBatchSize(gigabytes: number) { - setButtonValue(gigabytes) const capacity = Utils.getDepthForSize(Size.fromGigabytes(gigabytes)) - setDepthInput(capacity) + validateDepthInput(String(capacity), setDepthError, (v: string) => setDepthInput(Number(v))) + setButtonValue(gigabytes) } return ( @@ -162,6 +164,7 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen {getBatchValue(32)} {getBatchValue(256)} + {depthError && {depthError}} Data persistence @@ -200,7 +203,7 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen } bee={beeApi} - stamp={stamp.batchID} + stamp={stamp} + status={status.all} /> } bee={beeApi} - stamp={stamp.batchID} + stamp={stamp} + status={status.all} /> diff --git a/src/utils/identity.ts b/src/utils/identity.ts index d6bd9c05..9b73f03e 100644 --- a/src/utils/identity.ts +++ b/src/utils/identity.ts @@ -4,7 +4,8 @@ import { randomBytes, Wallet } from 'ethers' import { Identity, IdentityType } from '../providers/Feeds' import { LocalStorageKeys } from './localStorage' -import { uuidV4, waitUntilStampUsable } from '.' +import { waitUntilStampUsable } from './stamp' +import { uuidV4 } from '.' export function generateWallet(): Wallet { const privateKey = new Bytes(randomBytes(PrivateKey.LENGTH)).toString() diff --git a/src/utils/index.ts b/src/utils/index.ts index 5d1a8bf7..feb7a93f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,4 @@ -import { BatchId, Bee, PostageBatch, Reference } from '@ethersphere/bee-js' +import { Reference } from '@ethersphere/bee-js' import { BigNumber } from 'bignumber.js' import { BZZ_LINK_DOMAIN } from '../constants' @@ -207,36 +207,3 @@ export function shortenText(text: string, length = 20, separator = '[…]'): str return `${text.slice(0, length)}${separator}${text.slice(-length)}` } - -const DEFAULT_POLLING_FREQUENCY = 1_000 -const DEFAULT_STAMP_USABLE_TIMEOUT = 5 * 60_000 - -interface Options { - pollingFrequency?: number - timeout?: number -} - -export function waitUntilStampUsable(batchId: BatchId | string, bee: Bee, options?: Options): Promise { - return waitForStamp(batchId, bee, options) -} - -async function waitForStamp(batchId: BatchId | string, bee: Bee, options?: Options): Promise { - const timeout = options?.timeout || DEFAULT_STAMP_USABLE_TIMEOUT - const pollingFrequency = options?.pollingFrequency || DEFAULT_POLLING_FREQUENCY - - for (let i = 0; i < timeout; i += pollingFrequency) { - try { - const stamp = await bee.getPostageBatch(batchId) - - if (stamp.usable) { - return stamp - } - } catch { - // ignore - } - - await sleepMs(pollingFrequency) - } - - throw new Error('Wait until stamp usable timeout has been reached') -} diff --git a/src/utils/stamp.ts b/src/utils/stamp.ts new file mode 100644 index 00000000..6ed87d35 --- /dev/null +++ b/src/utils/stamp.ts @@ -0,0 +1,63 @@ +import { BatchId, Bee, PostageBatch } from '@ethersphere/bee-js' +import BigNumber from 'bignumber.js' + +import { MAX_STAMP_DEPTH, MIN_STAMP_DEPTH } from '../constants' + +import { sleepMs } from '.' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function validateDepthInput(depthInput: string, onError: (v: any) => void, onSuccess: (v: any) => void) { + let validDepthInput = '0' + + if (!depthInput) { + onError('Required field') + } else { + const depth = new BigNumber(depthInput) + + if (!depth.isInteger()) { + onError('Depth must be an integer') + } else if (depth.isLessThan(MIN_STAMP_DEPTH)) { + onError(`Minimal depth is ${MIN_STAMP_DEPTH}`) + } else if (depth.isGreaterThan(MAX_STAMP_DEPTH)) { + onError(`Depth has to be at most ${MAX_STAMP_DEPTH}`) + } else { + onError('') + validDepthInput = depthInput + } + } + + onSuccess(validDepthInput) +} + +const DEFAULT_POLLING_FREQUENCY = 1_000 +const DEFAULT_STAMP_USABLE_TIMEOUT = 5 * 60_000 + +interface Options { + pollingFrequency?: number + timeout?: number +} + +export function waitUntilStampUsable(batchId: BatchId | string, bee: Bee, options?: Options): Promise { + return waitForStamp(batchId, bee, options) +} + +async function waitForStamp(batchId: BatchId | string, bee: Bee, options?: Options): Promise { + const timeout = options?.timeout || DEFAULT_STAMP_USABLE_TIMEOUT + const pollingFrequency = options?.pollingFrequency || DEFAULT_POLLING_FREQUENCY + + for (let i = 0; i < timeout; i += pollingFrequency) { + try { + const stamp = await bee.getPostageBatch(batchId) + + if (stamp.usable) { + return stamp + } + } catch { + // ignore + } + + await sleepMs(pollingFrequency) + } + + throw new Error('Wait until stamp usable timeout has been reached') +} diff --git a/vite.config.mts b/vite.config.mts index 84ef7e96..9878e795 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -59,7 +59,7 @@ export default defineConfig(({ mode }) => { plugins: [ react(), nodePolyfills({ - include: ['util', 'buffer'], + include: ['util', 'buffer', 'stream'], globals: { Buffer: true, global: true, From b33b6630c2b5830b0fdbfbcf14cadc3fa1225190 Mon Sep 17 00:00:00 2001 From: rolandlor <33499567+rolandlor@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:31:44 +0200 Subject: [PATCH 06/11] fix: spdv-917 (#243) * fix: spdv-917 * refactor: spdv-917 --- .../stamps/PostageStampAdvancedCreation.tsx | 6 +-- .../stamps/PostageStampStandardCreation.tsx | 48 +++++++++++-------- src/pages/stamps/StampsTable.tsx | 4 +- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/pages/stamps/PostageStampAdvancedCreation.tsx b/src/pages/stamps/PostageStampAdvancedCreation.tsx index 4dc68428..dba416d8 100644 --- a/src/pages/stamps/PostageStampAdvancedCreation.tsx +++ b/src/pages/stamps/PostageStampAdvancedCreation.tsx @@ -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' @@ -157,7 +157,7 @@ export function PostageStampAdvancedCreation({ onFinished }: Props): ReactElemen } const theoreticalMaximumVolume = getHumanReadableFileSize(Utils.getStampTheoreticalBytes(depth)) - const effectiveVolume = getHumanReadableFileSize(Utils.getStampEffectiveBytes(depth)) + const effectiveVolume = getHumanReadableFileSize(Utils.getStampEffectiveBytes(depth, false, RedundancyLevel.OFF)) return ( @@ -225,7 +225,7 @@ export function PostageStampAdvancedCreation({ onFinished }: Props): ReactElemen setImmutable(event.target.value === 'Yes')} options={[ { value: 'Yes', label: 'Yes' }, diff --git a/src/pages/stamps/PostageStampStandardCreation.tsx b/src/pages/stamps/PostageStampStandardCreation.tsx index bc53cf76..fb8f78ed 100644 --- a/src/pages/stamps/PostageStampStandardCreation.tsx +++ b/src/pages/stamps/PostageStampStandardCreation.tsx @@ -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,6 +8,7 @@ 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' @@ -49,13 +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(Utils.getDepthForSize(Size.fromGigabytes(4))) const [amountInput, setAmountInput] = useState(Utils.getAmountForDuration(Duration.fromDays(30), 26500, 5)) - const [labelInput, setLabelInput] = useState('') - const [submitting, setSubmitting] = useState(false) - const [buttonValue, setButtonValue] = useState(4) + const [labelInput, setLabelInput] = useState('') + const [submitting, setSubmitting] = useState(false) + const [buttonValue, setButtonValue] = useState(4) const [depthError, setDepthError] = useState('') + const [sliderValue, setSliderValue] = useState(30) + + const pricePerBlockDefault = 24000 + const currentPrice = chainState?.currentPrice ?? pricePerBlockDefault const getBatchValue = (value: number) => { return ( @@ -76,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 { @@ -108,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) { @@ -128,9 +133,9 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen } function handleBatchSize(gigabytes: number) { - const capacity = Utils.getDepthForSize(Size.fromGigabytes(gigabytes)) - validateDepthInput(String(capacity), setDepthError, (v: string) => setDepthInput(Number(v))) setButtonValue(gigabytes) + const capacity = Utils.getDepthForSize(Size.fromGigabytes(gigabytes), false, RedundancyLevel.OFF) + validateDepthInput(String(capacity), setDepthError, (v: string) => setDepthInput(Number(v))) } return ( @@ -186,11 +191,12 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen Corresponding TTL (Time to live) {amountInput ? getTtl(amountInput) : '-'} + {amountInput ? getTtl(amountInput) : '-'} - Current price of 24000 PLUR per block + Current price of {currentPrice} PLUR per block diff --git a/src/pages/stamps/StampsTable.tsx b/src/pages/stamps/StampsTable.tsx index 225de54a..d03597e5 100644 --- a/src/pages/stamps/StampsTable.tsx +++ b/src/pages/stamps/StampsTable.tsx @@ -39,8 +39,8 @@ function StampsTable({ postageStamps }: Props): ReactElement | null { From f943f7ad666de15ef780cb5adf736b533902eef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferenc=20S=C3=A1rai?= Date: Thu, 2 Apr 2026 14:10:36 +0200 Subject: [PATCH 07/11] fix: add syncing message for Bee node and update page state handling spdv-955 (#244) --- src/pages/filemanager/index.tsx | 46 +++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/src/pages/filemanager/index.tsx b/src/pages/filemanager/index.tsx index 106c1ca5..fa1254bf 100644 --- a/src/pages/filemanager/index.tsx +++ b/src/pages/filemanager/index.tsx @@ -95,6 +95,22 @@ function LoadingBlock() { ) } +function ChainSyncingBlock() { + return ( +
+
+ +
+ ) +} + function ErrorModalBlock({ onClick, label }: { onClick: () => void; label: string }) { return } @@ -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(false) const [cacheHelpUrl, setCacheHelpUrl] = useState(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 (setupIncomplete && isChainSyncing) return PageState.ChainSyncing - if (!hasAdminStamp && !hasAdminDrive && !isCreationInProgress) return PageState.Initial + 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 } + if (pageState === PageState.ChainSyncing) { + return + } + if (pageState === PageState.NoPrivateKey) { return } @@ -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 ( { setAdminShowErrorModal(false) setErrorMessage('') From 056188abedf3a8ac828b8eb10a71a3b823cd5e6e Mon Sep 17 00:00:00 2001 From: rolandlor <33499567+rolandlor@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:14:54 +0200 Subject: [PATCH 08/11] fix: spdv-1037 (#245) --- src/pages/stamps/PostageStampStandardCreation.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/stamps/PostageStampStandardCreation.tsx b/src/pages/stamps/PostageStampStandardCreation.tsx index fb8f78ed..67ac0d45 100644 --- a/src/pages/stamps/PostageStampStandardCreation.tsx +++ b/src/pages/stamps/PostageStampStandardCreation.tsx @@ -191,7 +191,6 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen Corresponding TTL (Time to live) {amountInput ? getTtl(amountInput) : '-'} - {amountInput ? getTtl(amountInput) : '-'} From 8b36556502d316ac5bd7dba49ce34b594857d449 Mon Sep 17 00:00:00 2001 From: rolandlor <33499567+rolandlor@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:09:43 +0200 Subject: [PATCH 09/11] fix: spdv-1038 (#246) * fix: spdv-1038 * refactor: spdv-1038 --- src/pages/filemanager/index.tsx | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/pages/filemanager/index.tsx b/src/pages/filemanager/index.tsx index fa1254bf..07462b89 100644 --- a/src/pages/filemanager/index.tsx +++ b/src/pages/filemanager/index.tsx @@ -1,3 +1,4 @@ +import { BeeModes } from '@ethersphere/bee-js' import { DriveInfo, FileManagerBase } from '@solarpunkltd/file-manager-lib' import { ReactElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' @@ -44,6 +45,18 @@ function InitializationErrorBlock({ onOk }: { onOk: () => void }) { ) } +function UltraLightNodeErrorBlock() { + return ( +
+
+
+ File Manager is not available with an Ultra-light node. Please upgrade to a Light node to continue. +
+
+
+ ) +} + function ResetModalBlock({ cacheHelpUrl, onConfirm }: { cacheHelpUrl: string; onConfirm: () => void }) { return (
@@ -169,6 +182,7 @@ function FileManagerMainContent(props: { enum PageState { Connecting = 'connecting', // still warming up — show nothing / loader + UltraLightNode = 'ultra-light-node', // ultra-light node — file manager not available NoPrivateKey = 'no-pk', // private key not set Loading = 'loading', // bee ready, pk present, FM init in progress Reset = 'reset', // STATE_INVALID emitted and user has not yet acknowledged @@ -189,7 +203,7 @@ export function FileManagerPage(): ReactElement { const [connectionErrorDismissed, setConnectionErrorDismissed] = useState(false) const [cacheHelpUrl, setCacheHelpUrl] = useState(cacheClearUrls[BrowserPlatform.Chrome]) - const { status, chainState } = useContext(BeeContext) + const { status, chainState, nodeInfo } = useContext(BeeContext) const { fm, initDone, shallReset, adminDrive, initializationError, notifyPkSaved } = useContext(FMContext) useEffect(() => { @@ -228,6 +242,8 @@ export function FileManagerPage(): ReactElement { if (!isBeeReady && !initDone) return PageState.Connecting + if (nodeInfo?.beeMode === BeeModes.ULTRA_LIGHT) return PageState.UltraLightNode + if (!hasPk) return PageState.NoPrivateKey if (!initDone) return PageState.Loading @@ -259,6 +275,7 @@ export function FileManagerPage(): ReactElement { adminDrive, isCreationInProgress, chainState, + nodeInfo?.beeMode, ]) const handlePrivateKeySaved = useCallback(() => { @@ -274,6 +291,10 @@ export function FileManagerPage(): ReactElement { const loading = !fm?.adminStamp || !adminDrive const isFormbricksActive = Boolean(fm && fm.adminStamp && adminDrive && !loading) + if (pageState === PageState.UltraLightNode) { + return + } + if (pageState === PageState.Connecting || pageState === PageState.Loading) { return } From 97321706c33fb02abe7e067e6d865a046051d68b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1lint=20Ujv=C3=A1ri?= <58116288+bosi95@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:44:26 +0200 Subject: [PATCH 10/11] fix: validate stamp before every upgrade click (#247) * fix: validate stamp before every upgrade click --------- Co-authored-by: Roland Seres --- .../ExpiringNotificationModal.tsx | 20 +++++++++++++++++-- .../components/FileBrowser/FileBrowser.tsx | 9 +++++++-- .../NotificationBar/NotificationBar.tsx | 6 ++++-- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/modules/filemanager/components/ExpiringNotificationModal/ExpiringNotificationModal.tsx b/src/modules/filemanager/components/ExpiringNotificationModal/ExpiringNotificationModal.tsx index 69153256..2ee61b85 100644 --- a/src/modules/filemanager/components/ExpiringNotificationModal/ExpiringNotificationModal.tsx +++ b/src/modules/filemanager/components/ExpiringNotificationModal/ExpiringNotificationModal.tsx @@ -1,9 +1,10 @@ -import { PostageBatch } from '@ethersphere/bee-js' +import { Bee, PostageBatch } from '@ethersphere/bee-js' import { DriveInfo, FileInfo } from '@solarpunkltd/file-manager-lib' import { ReactElement, useEffect, useMemo, useState } from 'react' import { createPortal } from 'react-dom' import AlertIcon from 'remixicon-react/AlertLineIcon' +import { validateStampStillExists } from '../../utils/bee' import { getDaysLeft } from '../../utils/common' import { Button } from '../Button/Button' import { UpgradeDriveModal } from '../UpgradeDriveModal/UpgradeDriveModal' @@ -16,19 +17,23 @@ import '../../styles/global.scss' const EXPIRING_ITEMS_PAGE_SIZE = 3 interface ExpiringNotificationModalProps { + bee: Bee stamps: PostageBatch[] drives: DriveInfo[] files: FileInfo[] onCancelClick: () => void setErrorMessage?: (error: string) => void + setShowError: (show: boolean) => void } export function ExpiringNotificationModal({ + bee, stamps, drives, files, onCancelClick, setErrorMessage, + setShowError, }: ExpiringNotificationModalProps): ReactElement { const [showUpgradeDriveModal, setShowUpgradeDriveModal] = useState(false) const [actualStamp, setActualStamp] = useState(undefined) @@ -75,7 +80,18 @@ export function ExpiringNotificationModal({ files={files} currentPage={currentPage} index={index} - onUpgradeClick={(stamp, drive) => { + onUpgradeClick={async (stamp, drive) => { + const isStampValid = await validateStampStillExists(bee, stamp.batchID) + + if (!isStampValid) { + setErrorMessage?.( + `Drive ${drive.name} has expired. Please clear the browser cache and reload the page.`, + ) + setShowError(true) + + return + } + setActualStamp(stamp) setActualDrive(drive) setShowUpgradeDriveModal(true) diff --git a/src/modules/filemanager/components/FileBrowser/FileBrowser.tsx b/src/modules/filemanager/components/FileBrowser/FileBrowser.tsx index 40d93c89..f448e988 100644 --- a/src/modules/filemanager/components/FileBrowser/FileBrowser.tsx +++ b/src/modules/filemanager/components/FileBrowser/FileBrowser.tsx @@ -10,6 +10,7 @@ import React, { useRef, useState, } from 'react' +import { createPortal } from 'react-dom' import { useSearch } from '../../../../pages/filemanager/SearchContext' import { useView } from '../../../../pages/filemanager/ViewContext' @@ -87,7 +88,9 @@ function ErrorModalBlock({ return null } - return + const modalRoot = document.querySelector('.fm-main') || document.body + + return createPortal(, modalRoot) } const extractFilesFromClipboardEvent = (e: React.ClipboardEvent): File[] => { @@ -431,8 +434,10 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps) if (rafIdRef.current) { cancelAnimationFrame(rafIdRef.current) } + + setShowError(false) } - }, []) + }, [setShowError]) useEffect(() => { let title = currentDrive?.name || '' diff --git a/src/modules/filemanager/components/NotificationBar/NotificationBar.tsx b/src/modules/filemanager/components/NotificationBar/NotificationBar.tsx index 5caba9dd..5719bc6b 100644 --- a/src/modules/filemanager/components/NotificationBar/NotificationBar.tsx +++ b/src/modules/filemanager/components/NotificationBar/NotificationBar.tsx @@ -30,7 +30,7 @@ export function NotificationBar({ setErrorMessage }: NotificationBarProps): Reac const [stampsToExpire, setStampsToExpire] = useState([]) const [drivesToExpire, setDrivesToExpire] = useState([]) const { beeApi } = useContext(SettingsContext) - const { drives, files, adminDrive } = useContext(FMContext) + const { drives, files, adminDrive, setShowError } = useContext(FMContext) const showExpiration = stampsToExpire.length > 0 @@ -109,8 +109,9 @@ export function NotificationBar({ setErrorMessage }: NotificationBarProps): Reac
setShowExpiringModal(true)}> {stampsToExpire.length} drive{stampsToExpire.length > 1 ? 's' : ''} expiring soon
- {showExpiringModal && ( + {showExpiringModal && beeApi && ( )} From f52ed4abb2bb5274b33430c1e8efadae6b3fa795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferenc=20S=C3=A1rai?= Date: Thu, 9 Apr 2026 16:55:03 +0200 Subject: [PATCH 11/11] fix: use tochecksum() and toplurbigint() for ethers v6 compatibility (#248) --- src/utils/rpc.ts | 4 +-- tests/unit/rpc.spec.ts | 56 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/utils/rpc.ts b/src/utils/rpc.ts index eb0984c6..96bf89c3 100644 --- a/src/utils/rpc.ts +++ b/src/utils/rpc.ts @@ -84,7 +84,7 @@ export async function sendNativeTransaction( const feedData = await signer.provider.getFeeData() const gasPrice = externalGasPrice ?? DAI.fromWei(feedData.gasPrice?.toString() || '0') const transaction = await signer.sendTransaction({ - to: to.toHex(), + to: to.toChecksum(), value: BigInt(value.toWeiString()), gasPrice: BigInt(gasPrice.toWeiString()), gasLimit: BigInt(21000), @@ -117,7 +117,7 @@ export async function sendBzzTransaction( const feeData = await signer.provider.getFeeData() const gasPrice = feeData.gasPrice || BigInt(0) const bzz = new Contract(BZZ_TOKEN_ADDRESS, bzzABI, signer) - const transaction = await bzz.transfer(to.toChecksum(), value, { gasPrice }) + const transaction = await bzz.transfer(to.toChecksum(), value.toPLURBigInt(), { gasPrice }) const receipt = await transaction.wait(1) if (!receipt) { diff --git a/tests/unit/rpc.spec.ts b/tests/unit/rpc.spec.ts index 4adba107..2009def5 100644 --- a/tests/unit/rpc.spec.ts +++ b/tests/unit/rpc.spec.ts @@ -1,6 +1,6 @@ -import { BZZ } from '@ethersphere/bee-js' +import { BZZ, DAI } from '@ethersphere/bee-js' -import { sendBzzTransaction } from '../../src/utils/rpc' +import { sendBzzTransaction, sendNativeTransaction } from '../../src/utils/rpc' interface MockProvider { getFeeData: jest.Mock @@ -11,12 +11,14 @@ const mockWait = jest.fn() const mockTransfer = jest.fn() const mockGetFeeData = jest.fn() const mockGetNetwork = jest.fn() +const mockSendTransaction = jest.fn() const mockProvider: MockProvider = { getFeeData: mockGetFeeData, getNetwork: mockGetNetwork, } -const value = BZZ.fromDecimalString('1') +const bzzValue = BZZ.fromDecimalString('1') +const daiValue = DAI.fromDecimalString('1') const privateKey = 'FFFF000000000000000000000000000000000000000000000000000000000000' const jsonRpcProvider = 'http://mock-json-rpc-provider' @@ -39,6 +41,7 @@ jest.mock('ethers', () => { class Wallet { provider: MockProvider + sendTransaction = mockSendTransaction constructor(_privateKey: string, provider: MockProvider) { this.provider = provider @@ -64,8 +67,53 @@ describe('sendBzzTransaction', () => { }) it.each(addresses)('sendBzzTransaction to address: %s', async (address: string) => { - await sendBzzTransaction(privateKey, address, value, jsonRpcProvider) + await sendBzzTransaction(privateKey, address, bzzValue, jsonRpcProvider) const to = mockTransfer.mock.calls[0][0] as string expect(to.startsWith('0x')).toBe(true) }) + + it('passes BZZ value as bigint (not BZZ object)', async () => { + await sendBzzTransaction(privateKey, '0x52908400098527886e0f7030069857d2e4169ee7', bzzValue, jsonRpcProvider) + const transferredValue = mockTransfer.mock.calls[0][1] + expect(typeof transferredValue).toBe('bigint') + expect(transferredValue).toBe(bzzValue.toPLURBigInt()) + }) +}) + +describe('sendNativeTransaction', () => { + const addresses = ['52908400098527886e0f7030069857d2e4169ee7', '0x52908400098527886e0f7030069857d2e4169ee7'] + + beforeEach(() => { + jest.clearAllMocks() + mockWait.mockResolvedValue({ status: 1 }) + mockSendTransaction.mockResolvedValue({ wait: mockWait }) + mockGetFeeData.mockResolvedValue({ gasPrice: BigInt(1) }) + mockGetNetwork.mockResolvedValue({ chainId: BigInt(100) }) + }) + + it.each(addresses)('sendNativeTransaction to address: %s passes 0x-prefixed address', async (address: string) => { + await sendNativeTransaction(privateKey, address, daiValue, jsonRpcProvider) + const tx = mockSendTransaction.mock.calls[0][0] as { to: string } + expect(tx.to.startsWith('0x')).toBe(true) + }) + + it('passes DAI value as bigint', async () => { + await sendNativeTransaction(privateKey, '0x52908400098527886e0f7030069857d2e4169ee7', daiValue, jsonRpcProvider) + const tx = mockSendTransaction.mock.calls[0][0] as { value: bigint } + expect(typeof tx.value).toBe('bigint') + expect(tx.value).toBe(BigInt(daiValue.toWeiString())) + }) + + it('uses externalGasPrice when provided', async () => { + const externalGasPrice = DAI.fromWei('9999') + await sendNativeTransaction( + privateKey, + '0x52908400098527886e0f7030069857d2e4169ee7', + daiValue, + jsonRpcProvider, + externalGasPrice, + ) + const tx = mockSendTransaction.mock.calls[0][0] as { gasPrice: bigint } + expect(tx.gasPrice).toBe(BigInt(externalGasPrice.toWeiString())) + }) })