Compare commits

...

10 Commits

Author SHA1 Message Date
OpenCode 29b138346e fix: adapt redistribution feature for v0.35.1
- Use @mui/material import in redistribution page

- Configure pnpm workspace build script approvals
2026-06-15 09:55:16 +00:00
woggioni b5b0d37e94 added new page with node redistribution statistics 2026-06-15 09:47:25 +00:00
bee-worker b3137fbef1 chore(master): release 0.35.1 (#732)
* chore(master): release 0.35.1
* docs: update changelog.md for v0.53.1 bug fixes

---------

Co-authored-by: Ferenc Sárai <sarai.ferenc@gmail.com>
2026-04-10 11:25:29 +02:00
Ferenc Sárai e6f882d7e1 fix: for upcoming v0.53.1 (#731)
* fix: swap error caused by invalid id and batchcount
* fix: enhance creation messages for admin drive and user drives (#238)
* fix: enhance creation messages for admin drive and user drives
* fix: update creation message to indicate longer processing time
* fix: identity and wallet creation (#240)
* fix: asset preview types
* fix: fm search unicode text
* fix: feed identity and stamp usage
* fix: ui display changes (#239)
* fix: ui layout changes

* fix: stamp buy and dilute (#242)

fix: vite polyfill warning for stream
refactor: stamp depth and amount validation

* fix: spdv-917 (#243)

* fix: spdv-917

* refactor: spdv-917

* fix: add syncing message for Bee node and update page state handling spdv-955 (#244)

* fix: spdv-1037 (#245)

* fix: spdv-1038 (#246)

* fix: spdv-1038

* refactor: spdv-1038

* fix: validate stamp before every upgrade click (#247)

* fix: validate stamp before every upgrade click
---------

Co-authored-by: Roland Seres <roland.seres90@gmail.com>

* fix: use tochecksum() and toplurbigint() for ethers v6 compatibility (#248)

---------

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>
Co-authored-by: Roland Seres <roland.seres90@gmail.com>
2026-04-10 10:59:20 +02:00
bee-worker eda0529dfd chore(master): release 0.35.0 (#728)
* chore(master): release 0.35.0
* fix: update changelog with new features and bug fixes for version 0.35.0

---------

Co-authored-by: Ferenc Sárai <sarai.ferenc@gmail.com>
2026-04-02 15:30:25 +02:00
Ferenc Sárai cb5adfe031 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>
2026-04-02 14:53:20 +02:00
Ferenc Sárai 4848b5be97 fix: remove cross-env from prepublishOnly script 2026-03-20 18:13:06 +01:00
Ferenc Sárai becb87377e fix: add workflow_dispatch trigger to npm publish workflow 2026-03-20 17:58:55 +01:00
bee-worker f149f86155 chore(master): release 0.34.0 (#727)
* chore(master): release 0.34.0

* fix: correct version number in package.json and update changelog

* fix: remove packageManager field from package.json

---------

Co-authored-by: Ferenc Sárai <sarai.ferenc@gmail.com>
2026-03-20 17:51:07 +01:00
Ferenc Sárai 37ab8fedaa fix: add --no-git-checks to pnpm publish command 2026-03-20 17:08:38 +01:00
54 changed files with 942 additions and 424 deletions
+4 -1
View File
@@ -1,3 +1,6 @@
PORT=3002 PORT=3002
VITE_BEE_DESKTOP_URL=http://localhost:3054
VITE_FORMBRICKS_ENV_ID= VITE_FORMBRICKS_ENV_ID=
VITE_FORMBRICKS_APP_URL= VITE_FORMBRICKS_APP_URL=
VITE_DEFAULT_RPC_URL=
VITE_BEE_DESKTOP_ENABLED=
+2 -1
View File
@@ -1,5 +1,6 @@
name: Publish on npm name: Publish on npm
on: on:
workflow_dispatch:
release: release:
types: [published] types: [published]
@@ -35,4 +36,4 @@ jobs:
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm publish --provenance --access public - run: pnpm publish --provenance --access public --no-git-checks
+1
View File
@@ -23,6 +23,7 @@
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
/**.log
settings.json settings.json
+73 -28
View File
@@ -1,42 +1,87 @@
# Changelog # Changelog
## [0.34.0](https://github.com/ethersphere/bee-dashboard/compare/v0.33.5...v0.34.0) (2026-03-20) ## [0.35.1](https://github.com/ethersphere/bee-dashboard/compare/v0.35.0...v0.35.1) (2026-04-10)
### Features
* add image opening functionality to DownloadActionBar ([#725](https://github.com/ethersphere/bee-dashboard/issues/725)) ([3ff645c](https://github.com/ethersphere/bee-dashboard/commit/3ff645cab1b4e9fba0c42ed99e7c3fac7b0ed0ca))
* spdv-995 ([#225](https://github.com/ethersphere/bee-dashboard/issues/225)) ([fa8a26e](https://github.com/ethersphere/bee-dashboard/commit/fa8a26e80d83d5c794a9d572cf716f0cb484e642))
* sync and update with all changes from fork ([#720](https://github.com/ethersphere/bee-dashboard/issues/720)) ([519c411](https://github.com/ethersphere/bee-dashboard/commit/519c411db0b37f3d4004366ca267013e2268f8eb))
### Bug Fixes ### Bug Fixes
* add error handling and ui notifications for download failures ([#217](https://github.com/ethersphere/bee-dashboard/issues/217)) ([b58f01c](https://github.com/ethersphere/bee-dashboard/commit/b58f01cc2b5535ba0ad347d03ae4102e18f637f0)) - [`cb6854e`](https://github.com/ethersphere/bee-dashboard/commit/cb6854eb68ffe3064a39a171bc1e23f628ee93bb) fix: swap error caused by invalid id and batchcount
- [`bb93d5c`](https://github.com/ethersphere/bee-dashboard/commit/bb93d5c26fa5414c6423b87a3992e0f2e410e515) fix: enhance creation messages for admin drive and user drives [(#238)](https://github.com/ethersphere/bee-dashboard/issues/238)
- [`c08bf8a`](https://github.com/ethersphere/bee-dashboard/commit/d65da143d2200db653fe7a80a7891dacf4c2937e) fix: identity and wallet creation [(#240)](https://github.com/ethersphere/bee-dashboard/pull/240)
- [`d65da14`](https://github.com/ethersphere/bee-dashboard/commit/d65da143d2200db653fe7a80a7891dacf4c2937e) fix: ui display changes [(#239)](https://github.com/ethersphere/bee-dashboard/issues/239)
- [`c890f7c`](https://github.com/ethersphere/bee-dashboard/commit/c890f7c1e8e4d21f8d252b3e1a9c783982459adf) fix: stamp buy and dilute [(#242)](https://github.com/ethersphere/bee-dashboard/issues/242)
- [`b33b663`](https://github.com/ethersphere/bee-dashboard/commit/b33b6630c2b5830b0fdbfbcf14cadc3fa1225190) fix: standard mode postage stamp purchase [(#243)](https://github.com/ethersphere/bee-dashboard/issues/243)
- [`f943f7a`](https://github.com/ethersphere/bee-dashboard/commit/f943f7ad666de15ef780cb5adf736b533902eef7) fix: add syncing message for bee node and update page state [(#244)](https://github.com/ethersphere/bee-dashboard/pull/244)
- [`056188a`](https://github.com/ethersphere/bee-dashboard/commit/056188abedf3a8ac828b8eb10a71a3b823cd5e6e) fix: duplicated ttl (time to live) information [(#245)](https://github.com/ethersphere/bee-dashboard/issues/245)
- [`8b36556`](https://github.com/ethersphere/bee-dashboard/commit/8b36556502d316ac5bd7dba49ce34b594857d449) fix: misleading "bee node is syncing" message for ultra-light nodes in file manager [(#246)](https://github.com/ethersphere/bee-dashboard/pull/246)
- [`9732170`](https://github.com/ethersphere/bee-dashboard/commit/97321706c33fb02abe7e067e6d865a046051d68b) fix: validate stamp before every upgrade click [(#247)](https://github.com/ethersphere/bee-dashboard/issues/247)
- [`f52ed4a`](https://github.com/ethersphere/bee-dashboard/commit/f52ed4abb2bb5274b33430c1e8efadae6b3fa795) fix: use tochecksum() and toplurbigint for ethers v6 compatibility [(#248)](https://github.com/ethersphere/bee-dashboard/pull/248)
## [0.35.0](https://github.com/ethersphere/bee-dashboard/compare/v0.34.0...v0.35.0) (2026-04-02)
### Features
* add image opening functionality to DownloadActionBar ([3ff645c](https://github.com/ethersphere/bee-dashboard/commit/3ff645cab1b4e9fba0c42ed99e7c3fac7b0ed0ca))
* the dashboard sidebar should be collapsible ([fa8a26e](https://github.com/ethersphere/bee-dashboard/commit/fa8a26e80d83d5c794a9d572cf716f0cb484e642))
* sync and update with all changes from fork ([519c411](https://github.com/ethersphere/bee-dashboard/commit/519c411db0b37f3d4004366ca267013e2268f8eb))
### Bug Fixes
* swap error caused by invalid id and batchcount
* enhance creation messages for admin drive and user drives
* identity and wallet creation
* asset preview types
* fm search unicode text
* feed identity and stamp usage
* ui display changes
* stamp buy and dilute
* vite polyfill warning for stream
* standard mode postage stamp purchase reserves incorrect size and duration
* add syncing message for Bee node and update page state handling
* stamp depth and amount validation
* add --no-git-checks to pnpm publish command ([37ab8fe](https://github.com/ethersphere/bee-dashboard/commit/37ab8fedaa9fa2f941b84746ec83fe87cf61b014))
* add workflow_dispatch trigger to npm publish workflow ([becb873](https://github.com/ethersphere/bee-dashboard/commit/becb87377e506aca0c429439d70e43151025d755))
* remove cross-env from prepublishOnly script ([4848b5b](https://github.com/ethersphere/bee-dashboard/commit/4848b5be97a827abeb0ca8156ef53cab0d2f315e))
## [0.34.0](https://github.com/ethersphere/bee-dashboard/compare/v0.33.5...v0.34.0) (2026-03-20)
### Features
* add image opening functionality to DownloadActionBar ([3ff645c](https://github.com/ethersphere/bee-dashboard/commit/3ff645cab1b4e9fba0c42ed99e7c3fac7b0ed0ca))
* the dashboard sidebar should be collapsible ([fa8a26e](https://github.com/ethersphere/bee-dashboard/commit/fa8a26e80d83d5c794a9d572cf716f0cb484e642))
* sync and update with all changes from fork ([519c411](https://github.com/ethersphere/bee-dashboard/commit/519c411db0b37f3d4004366ca267013e2268f8eb))
### Bug Fixes
* add error handling and ui notifications for download failures ([b58f01c](https://github.com/ethersphere/bee-dashboard/commit/b58f01cc2b5535ba0ad347d03ae4102e18f637f0))
* buy stamp values display ([3031d54](https://github.com/ethersphere/bee-dashboard/commit/3031d54272d51da020d3a3cd7154b969c60e8669)) * buy stamp values display ([3031d54](https://github.com/ethersphere/bee-dashboard/commit/3031d54272d51da020d3a3cd7154b969c60e8669))
* buy stamp values display ([#226](https://github.com/ethersphere/bee-dashboard/issues/226)) ([3031d54](https://github.com/ethersphere/bee-dashboard/commit/3031d54272d51da020d3a3cd7154b969c60e8669)) * cannot forget expired drives ([7e05a56](https://github.com/ethersphere/bee-dashboard/commit/7e05a56073a2be306a1394bf5b2e798a1a457c74))
* cannot forget expired drives ([#214](https://github.com/ethersphere/bee-dashboard/issues/214)) ([7e05a56](https://github.com/ethersphere/bee-dashboard/commit/7e05a56073a2be306a1394bf5b2e798a1a457c74)) * Correct misleading update warning message for desktop version ([bc2c0ad](https://github.com/ethersphere/bee-dashboard/commit/bc2c0addbb685454fbae61d317f2e920d507c07c))
* correct misleading update warning message for desktop version ([#218](https://github.com/ethersphere/bee-dashboard/issues/218)) ([bc2c0ad](https://github.com/ethersphere/bee-dashboard/commit/bc2c0addbb685454fbae61d317f2e920d507c07c))
* dockerfile build and use compose ([76bf221](https://github.com/ethersphere/bee-dashboard/commit/76bf2211ca75f44255e5696d58072e639a9cd2b0)) * dockerfile build and use compose ([76bf221](https://github.com/ethersphere/bee-dashboard/commit/76bf2211ca75f44255e5696d58072e639a9cd2b0))
* download and upload files ([#223](https://github.com/ethersphere/bee-dashboard/issues/223)) ([e8e01c0](https://github.com/ethersphere/bee-dashboard/commit/e8e01c054a867447e07a71960d45f9cb36926e61)) * download and upload files ([e8e01c0](https://github.com/ethersphere/bee-dashboard/commit/e8e01c054a867447e07a71960d45f9cb36926e61))
* filemanager state handling ([#232](https://github.com/ethersphere/bee-dashboard/issues/232)) ([855a017](https://github.com/ethersphere/bee-dashboard/commit/855a0170334fb36d8ff119ce96f8c9bdb4294d58)) * filemanager state handling ([855a017](https://github.com/ethersphere/bee-dashboard/commit/855a0170334fb36d8ff119ce96f8c9bdb4294d58))
* reflect file rename immediately ([#227](https://github.com/ethersphere/bee-dashboard/issues/227)) ([a56a5c3](https://github.com/ethersphere/bee-dashboard/commit/a56a5c3ed8554cf81afc7c6401ca2dc65a8e898d)) * reflect file rename immediately ([a56a5c3](https://github.com/ethersphere/bee-dashboard/commit/a56a5c3ed8554cf81afc7c6401ca2dc65a8e898d))
* **rpc:** ensure 0x prefix for recipient in sendBzzTransaction and add tests ([#722](https://github.com/ethersphere/bee-dashboard/issues/722)) ([fff94d9](https://github.com/ethersphere/bee-dashboard/commit/fff94d907157d0cd4f6e490c450cdc58cefeec31)) * rpc: ensure 0x prefix for recipient in sendBzzTransaction and add tests ([fff94d9](https://github.com/ethersphere/bee-dashboard/commit/fff94d907157d0cd4f6e490c450cdc58cefeec31))
* **sidebar:** use swarm-desktop GitHub link when running in desktop mode ([#724](https://github.com/ethersphere/bee-dashboard/issues/724)) ([7382da8](https://github.com/ethersphere/bee-dashboard/commit/7382da8595e86230c03dab93aec726cd02cc91ec)) * sidebar: use swarm-desktop GitHub link when running in desktop mode ([7382da8](https://github.com/ethersphere/bee-dashboard/commit/7382da8595e86230c03dab93aec726cd02cc91ec))
* spdv-1007 cost values correcting ([#234](https://github.com/ethersphere/bee-dashboard/issues/234)) ([a7e4205](https://github.com/ethersphere/bee-dashboard/commit/a7e42053ae99990c5e9e782e8fe14326f155730c)) * cost values correcting [a7e4205](https://github.com/ethersphere/bee-dashboard/commit/a7e42053ae99990c5e9e782e8fe14326f155730c))
* spdv-914 - Modals are partially cut off in File Manager on Windows (Chrome) ([#219](https://github.com/ethersphere/bee-dashboard/issues/219)) ([220618f](https://github.com/ethersphere/bee-dashboard/commit/220618f19bc95b92a0521dc62eb8f5414dfdcbba)) * modals are partially cut off in File Manager on Windows (Chrome) ([220618f](https://github.com/ethersphere/bee-dashboard/commit/220618f19bc95b92a0521dc62eb8f5414dfdcbba))
* spdv-934 ([#231](https://github.com/ethersphere/bee-dashboard/issues/231)) ([55e7879](https://github.com/ethersphere/bee-dashboard/commit/55e78798492e715ef9aa92262f5aa874aec240b0)) * misleading update warning (Desktop vs Dashboard) ([55e7879](https://github.com/ethersphere/bee-dashboard/commit/55e78798492e715ef9aa92262f5aa874aec240b0))
* spdv-963 - Expired admin drive upgrading handling ([#230](https://github.com/ethersphere/bee-dashboard/issues/230)) ([e1fdd52](https://github.com/ethersphere/bee-dashboard/commit/e1fdd52676653c65beba06283f07e470d7ee4668)) * expired admin drive upgrading handling ([e1fdd52](https://github.com/ethersphere/bee-dashboard/commit/e1fdd52676653c65beba06283f07e470d7ee4668))
* spdv-971 ([#211](https://github.com/ethersphere/bee-dashboard/issues/211)) ([e00918b](https://github.com/ethersphere/bee-dashboard/commit/e00918b192f9a916d72df7ed6790eddf908397d3)) * in case of a synced bee node the Purchase stamp and create drive button is inactive ([e00918b](https://github.com/ethersphere/bee-dashboard/commit/e00918b192f9a916d72df7ed6790eddf908397d3))
* spdv-973 ([3c4d618](https://github.com/ethersphere/bee-dashboard/commit/3c4d618cc85b2836a2f794b7da79f4da12205869)) * newly created drive sometimes appears under “Expired Drives” in Chrome until page refresh and sometimes you have to delete cache in order to appear. ([3c4d618](https://github.com/ethersphere/bee-dashboard/commit/3c4d618cc85b2836a2f794b7da79f4da12205869))
* spdv-986 [commit: 519c411d] - Admin drive stucks in "in progress" state and the already existing admin drives are not listed on the initial modal ([#215](https://github.com/ethersphere/bee-dashboard/issues/215)) ([ad8c8f1](https://github.com/ethersphere/bee-dashboard/commit/ad8c8f18eb5dabbf64e8059f0fcfe98a68b97eea)) * admin drive stucks in "in progress" state and the already existing admin drives are not listed on the initial modal ([ad8c8f1](https://github.com/ethersphere/bee-dashboard/commit/ad8c8f18eb5dabbf64e8059f0fcfe98a68b97eea))
* update documentation message for file access clarification ([#221](https://github.com/ethersphere/bee-dashboard/issues/221)) ([ae63615](https://github.com/ethersphere/bee-dashboard/commit/ae63615c7ca26896c1114c16dc6a921aa267bc0d)) * update documentation message for file access clarification ([ae63615](https://github.com/ethersphere/bee-dashboard/commit/ae63615c7ca26896c1114c16dc6a921aa267bc0d))
* upload history text align ([3031d54](https://github.com/ethersphere/bee-dashboard/commit/3031d54272d51da020d3a3cd7154b969c60e8669)) * upload history text align ([3031d54](https://github.com/ethersphere/bee-dashboard/commit/3031d54272d51da020d3a3cd7154b969c60e8669))
* upload q and drive size error ([#220](https://github.com/ethersphere/bee-dashboard/issues/220)) ([8992c18](https://github.com/ethersphere/bee-dashboard/commit/8992c189fd0300b0ee28d44a109c47b633174c06)) * upload and drive size error ([8992c18](https://github.com/ethersphere/bee-dashboard/commit/8992c189fd0300b0ee28d44a109c47b633174c06))
* use upload and download abort signals ([#212](https://github.com/ethersphere/bee-dashboard/issues/212)) ([308ec3d](https://github.com/ethersphere/bee-dashboard/commit/308ec3dcc0ff3806777b89dc99fef669c993bcf7)) * use upload and download abort signals ([308ec3d](https://github.com/ethersphere/bee-dashboard/commit/308ec3dcc0ff3806777b89dc99fef669c993bcf7))
* use uploadFile for single files to support long filenames and reafactor for linter ([#228](https://github.com/ethersphere/bee-dashboard/issues/228)) ([db52e44](https://github.com/ethersphere/bee-dashboard/commit/db52e4471a9519a0aca8a6fd2702a242e03d2a06)) * use uploadFile for single files to support long filenames and reafactor for linter ([db52e44](https://github.com/ethersphere/bee-dashboard/commit/db52e4471a9519a0aca8a6fd2702a242e03d2a06))
* withdraw and deposit buttons style ([3031d54](https://github.com/ethersphere/bee-dashboard/commit/3031d54272d51da020d3a3cd7154b969c60e8669)) * withdraw and deposit buttons style ([3031d54](https://github.com/ethersphere/bee-dashboard/commit/3031d54272d51da020d3a3cd7154b969c60e8669))
## [0.33.5](https://github.com/ethersphere/bee-dashboard/compare/v0.33.4...v0.33.5) (2026-02-17) ## [0.33.5](https://github.com/ethersphere/bee-dashboard/compare/v0.33.4...v0.33.5) (2026-02-17)
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ethersphere/bee-dashboard", "name": "@ethersphere/bee-dashboard",
"version": "0.34.0", "version": "0.35.1",
"description": "An app which helps users to setup their Bee node and do actions like cash out cheques", "description": "An app which helps users to setup their Bee node and do actions like cash out cheques",
"homepage": ".", "homepage": ".",
"bin": { "bin": {
@@ -27,7 +27,7 @@
"url": "https://github.com/ethersphere/bee-dashboard.git" "url": "https://github.com/ethersphere/bee-dashboard.git"
}, },
"scripts": { "scripts": {
"prepublishOnly": "cross-env NODE_ENV=production pnpm run prepare", "prepublishOnly": "NODE_ENV=production pnpm run prepare",
"prepare": "pnpm run build && pnpm run build:component", "prepare": "pnpm run build && pnpm run build:component",
"start": "vite", "start": "vite",
"desktop": "node ./desktop.mjs", "desktop": "node ./desktop.mjs",
+5
View File
@@ -0,0 +1,5 @@
allowBuilds:
'@parcel/watcher': false
esbuild: false
puppeteer: false
unrs-resolver: false
+1 -1
View File
@@ -40,7 +40,7 @@ export default function ExpandableListItem({ label, value, tooltip }: Props): Re
)} )}
{value && ( {value && (
<Box flex={1} textAlign="right"> <Box flex={1} textAlign="right">
<Typography variant="body2"> <Typography variant="body2" component="div">
{value} {value}
{tooltip && ( {tooltip && (
<Tooltip title={tooltip} placement="top" arrow> <Tooltip title={tooltip} placement="top" arrow>
+68 -60
View File
@@ -16,11 +16,17 @@ const useStyles = makeStyles()(theme => ({
header: { header: {
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
marginBottom: theme.spacing(0.25), 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', wordBreak: 'break-word',
'&:hover': {
backgroundColor: theme.palette.background.paper,
},
'&:focus-within': {
backgroundColor: theme.palette.background.paper,
},
}, },
headerOpen: { headerOpen: {
borderLeft: `${theme.spacing(0.25)}px solid ${theme.palette.primary.main}`, borderLeft: `${theme.spacing(0.25)} solid ${theme.palette.primary.main}`,
}, },
copyValue: { copyValue: {
cursor: 'pointer', cursor: 'pointer',
@@ -95,35 +101,35 @@ export default function ExpandableListItemInput({
} }
return ( return (
<ListItemButton className={`${classes.header} ${open ? classes.headerOpen : ''}`}> <>
<Box display="flex" flexDirection="column" width="100%"> <ListItemButton className={`${classes.header} ${open ? classes.headerOpen : ''}`}>
<Box display="flex" flexDirection="row" alignItems="center" width="100%"> <Box display="flex" flexDirection="column" width="100%">
{label && ( <Box display="flex" flexDirection="row" alignItems="center" width="100%">
<Box flex={1} minWidth={0}> {label && (
<Typography variant="body1" className={classes.unselectableLabel} component="span"> <Box flex={1} minWidth={0}>
{label} <Typography variant="body1" className={classes.unselectableLabel} component="span">
</Typography> {label}
</Typography>
</Box>
)}
<Box flex={3} display="flex" alignItems="center" justifyContent="flex-end" minWidth={0} gap={1}>
{!open && value && (
<Typography
variant="body2"
component="span"
sx={{ minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
>
{value}
</Typography>
)}
{!expandedOnly && !locked && (
<IconButton size="small" className={classes.copyValue} onClick={toggleOpen}>
{open ? <Minus strokeWidth={1} /> : <Edit strokeWidth={1} />}
</IconButton>
)}
</Box> </Box>
)}
<Box flex={3} display="flex" alignItems="center" justifyContent="flex-end" minWidth={0} gap={1}>
{!open && value && (
<Typography
variant="body2"
component="span"
sx={{ minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
>
{value}
</Typography>
)}
{!expandedOnly && !locked && (
<IconButton size="small" className={classes.copyValue} onClick={toggleOpen}>
{open ? <Minus strokeWidth={1} /> : <Edit strokeWidth={1} />}
</IconButton>
)}
</Box> </Box>
</Box> <Collapse in={open} timeout="auto" unmountOnExit>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box display="flex" flexDirection="column" width="100%">
<Box display="flex" alignItems="center" width="100%" minWidth={0}> <Box display="flex" alignItems="center" width="100%" minWidth={0}>
<InputBase <InputBase
value={inputValue} value={inputValue}
@@ -146,36 +152,38 @@ export default function ExpandableListItemInput({
/> />
</Box> </Box>
{helperText && <ExpandableListItemNote>{helperText}</ExpandableListItemNote>} {helperText && <ExpandableListItemNote>{helperText}</ExpandableListItemNote>}
<Box mt={2}> </Collapse>
<ExpandableListItemActions> </Box>
<SwarmButton </ListItemButton>
disabled={ <Collapse in={open} timeout="auto" unmountOnExit>
loading || <Box mt={2}>
inputValue === value || <ExpandableListItemActions>
Boolean(confirmLabelDisabled) || <SwarmButton
(inputValue === '' && value === undefined) disabled={
} loading ||
loading={loading} inputValue === value ||
iconType={confirmIcon ?? Check} Boolean(confirmLabelDisabled) ||
onClick={() => { (inputValue === '' && value === undefined)
onConfirm?.(inputValue.trim()) }
}} loading={loading}
> iconType={confirmIcon ?? Check}
{confirmLabel || 'Save'} onClick={() => {
</SwarmButton> onConfirm?.(inputValue.trim())
<SwarmButton }}
disabled={loading || inputValue === value || inputValue === ''} >
iconType={X} {confirmLabel || 'Save'}
onClick={() => setInputValue(value || '')} </SwarmButton>
cancel <SwarmButton
> disabled={loading || inputValue === value || inputValue === ''}
Cancel iconType={X}
</SwarmButton> onClick={() => setInputValue(value || '')}
</ExpandableListItemActions> cancel
</Box> >
</Box> Cancel
</Collapse> </SwarmButton>
</Box> </ExpandableListItemActions>
</ListItemButton> </Box>
</Collapse>
</>
) )
} }
+9 -3
View File
@@ -10,11 +10,17 @@ const useStyles = makeStyles()(theme => ({
header: { header: {
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
marginBottom: theme.spacing(0.25), 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', wordBreak: 'break-word',
'&:hover': {
backgroundColor: theme.palette.background.paper,
},
'&:focus-within': {
backgroundColor: theme.palette.background.paper,
},
}, },
headerOpen: { headerOpen: {
borderLeft: `${theme.spacing(0.25)}px solid ${theme.palette.primary.main}`, borderLeft: `${theme.spacing(0.25)} solid ${theme.palette.primary.main}`,
}, },
copyValue: { copyValue: {
cursor: 'pointer', cursor: 'pointer',
@@ -69,7 +75,7 @@ export default function ExpandableListItemKey({ label, value, expanded }: Props)
return ( return (
<ListItemButton className={`${classes.header} ${open ? classes.headerOpen : ''}`}> <ListItemButton className={`${classes.header} ${open ? classes.headerOpen : ''}`}>
<Grid container direction="column" justifyContent="space-between" alignItems="stretch"> <Grid container direction="column" justifyContent="space-between" alignItems="stretch" style={{ width: '100%' }}>
<Grid container direction="row" justifyContent="space-between" alignItems="center"> <Grid container direction="row" justifyContent="space-between" alignItems="center">
{label && ( {label && (
<Typography variant="body1" component="span"> <Typography variant="body1" component="span">
+8 -2
View File
@@ -10,11 +10,17 @@ const useStyles = makeStyles()(theme => ({
header: { header: {
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
marginBottom: theme.spacing(0.25), 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', wordBreak: 'break-word',
'&:hover': {
backgroundColor: theme.palette.background.paper,
},
'&:focus-within': {
backgroundColor: theme.palette.background.paper,
},
}, },
headerOpen: { headerOpen: {
borderLeft: `${theme.spacing(0.25)}px solid ${theme.palette.primary.main}`, borderLeft: `${theme.spacing(0.25)} solid ${theme.palette.primary.main}`,
}, },
openLinkIcon: { openLinkIcon: {
cursor: 'pointer', cursor: 'pointer',
+73
View File
@@ -0,0 +1,73 @@
import { BZZ, DAI, RedistributionState } from '@ethersphere/bee-js'
import { useContext, useEffect, useState } from 'react'
import { Context } from '../providers/Settings'
import ExpandableListItem from './ExpandableListItem'
export function Redistribution() {
const { beeApi } = useContext(Context)
const [redistributionState, setRedistributionState] = useState<RedistributionState | null>(null)
useEffect(() => {
const interval = setInterval(() => {
if (!beeApi) {
return
}
beeApi.getRedistributionState().then(setRedistributionState).catch(console.error) // eslint-disable-line
}, 3_000)
return () => clearInterval(interval)
})
const formatDurationSeconds = (s?: number) => {
if (s === null || s === undefined) {
return '-'
} else {
return `${s} s`
}
}
const formatBzzAmount = (amount?: BZZ) => {
if (amount === null || amount === undefined) {
return '-'
} else {
return `${amount.toSignificantDigits(4)} xBZZ`
}
}
const formatDaiAmount = (amount?: DAI) => {
if (amount === null || amount === undefined) {
return '-'
} else {
return `${amount.toSignificantDigits(4)} xDAI`
}
}
return (
<>
<ExpandableListItem
label="Has sufficient funds"
value={redistributionState?.hasSufficientFunds?.toString() ?? '-'}
/>
<ExpandableListItem label="Fully synced" value={redistributionState?.isFullySynced?.toString() ?? '-'} />
<ExpandableListItem label="Frozen" value={redistributionState?.isFrozen?.toString() ?? '-'} />
<ExpandableListItem label="Phase" value={redistributionState?.phase ?? '-'} />
<ExpandableListItem label="Round" value={redistributionState?.round?.toString() ?? '-'} />
<ExpandableListItem
label="Last selected round"
value={redistributionState?.lastSelectedRound.toString() ?? '-'}
/>
<ExpandableListItem label="Last played round" value={redistributionState?.lastPlayedRound.toString() ?? '-'} />
<ExpandableListItem label="Last round won" value={redistributionState?.lastWonRound.toString() ?? '-'} />
<ExpandableListItem label="Last frozen round" value={redistributionState?.lastFrozenRound.toString() ?? '-'} />
<ExpandableListItem
label="Last sample duration"
value={formatDurationSeconds(redistributionState?.lastSampleDurationSeconds)}
/>
<ExpandableListItem label="Reward" value={formatBzzAmount(redistributionState?.reward)} />
<ExpandableListItem label="Fees" value={formatDaiAmount(redistributionState?.fees)} />
</>
)
}
+7 -1
View File
@@ -4,6 +4,7 @@ import { ReactElement, useContext, useState } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import FilesIcon from 'remixicon-react/ArrowUpDownLineIcon' import FilesIcon from 'remixicon-react/ArrowUpDownLineIcon'
import DocsIcon from 'remixicon-react/BookOpenLineIcon' import DocsIcon from 'remixicon-react/BookOpenLineIcon'
import ExchangeDollarLineIcon from 'remixicon-react/ExchangeDollarLineIcon'
import ExternalLinkIcon from 'remixicon-react/ExternalLinkLineIcon' import ExternalLinkIcon from 'remixicon-react/ExternalLinkLineIcon'
import FileManagerIcon from 'remixicon-react/FolderOpenLineIcon' import FileManagerIcon from 'remixicon-react/FolderOpenLineIcon'
import GithubIcon from 'remixicon-react/GithubFillIcon' import GithubIcon from 'remixicon-react/GithubFillIcon'
@@ -139,7 +140,7 @@ export default function SideBar(): ReactElement {
label: 'File Manager', label: 'File Manager',
path: ROUTES.FILEMANAGER, path: ROUTES.FILEMANAGER,
icon: FileManagerIcon, icon: FileManagerIcon,
pathMatcherSubstring: '/filemanager/', pathMatcherSubstring: '/filemanager',
}, },
{ {
label: 'Account', label: 'Account',
@@ -147,6 +148,11 @@ export default function SideBar(): ReactElement {
icon: AccountIcon, icon: AccountIcon,
pathMatcherSubstring: '/account/', pathMatcherSubstring: '/account/',
}, },
{
label: 'Redistribution',
path: ROUTES.REDISTRIBUTION,
icon: ExchangeDollarLineIcon,
},
{ {
label: 'Settings', label: 'Settings',
path: ROUTES.SETTINGS, path: ROUTES.SETTINGS,
+95 -17
View File
@@ -1,4 +1,4 @@
import { BatchId, Bee } from '@ethersphere/bee-js' import { Bee, PostageBatch } from '@ethersphere/bee-js'
import { Box } from '@mui/material' import { Box } from '@mui/material'
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
import Dialog from '@mui/material/Dialog' import Dialog from '@mui/material/Dialog'
@@ -8,19 +8,48 @@ import DialogTitle from '@mui/material/DialogTitle'
import Input from '@mui/material/Input' import Input from '@mui/material/Input'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import React, { ReactElement, ReactNode, useState } from 'react' import React, { ReactElement, ReactNode, useState } from 'react'
import { makeStyles } from 'tss-react/mui'
interface Props { import { CheckState } from '../providers/Bee'
type: 'Topup' | 'Dilute'
icon: ReactNode const useStyles = makeStyles()(theme => ({
bee: Bee buttonSelected: {
stamp: BatchId color: 'white',
backgroundColor: theme.palette.primary.main,
'&:hover': {
color: theme.palette.secondary.main,
backgroundColor: 'white',
'@media (hover: none)': {
color: 'white',
backgroundColor: theme.palette.primary.main,
},
},
},
buttonUnselected: {
color: '#dd7700',
backgroundColor: 'white',
},
}))
export enum StampExtensionType {
Topup = 'Topup',
Dilute = 'Dilute',
} }
export default function StampExtensionModal({ type, icon, bee, stamp }: Props): ReactElement { interface Props {
const [open, setOpen] = useState(false) type: StampExtensionType
const [amount, setAmount] = useState('') icon: ReactNode
bee: Bee
stamp: PostageBatch
status: CheckState
}
export default function StampExtensionModal({ type, icon, bee, stamp, status }: Props): ReactElement {
const { classes } = useStyles()
const [open, setOpen] = useState<boolean>(false)
const [amount, setAmount] = useState<string>('')
const { enqueueSnackbar } = useSnackbar() 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<HTMLButtonElement>) => { const handleClickOpen = (e: React.MouseEvent<HTMLButtonElement>) => {
setOpen(true) setOpen(true)
@@ -32,23 +61,65 @@ export default function StampExtensionModal({ type, icon, bee, stamp }: Props):
} }
const handleAction = async () => { 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 { try {
await bee.topUpBatch(stamp, amount) await bee.topUpBatch(stamp.batchID, amount)
enqueueSnackbar(`Successfully topped up stamp, your changes will appear soon`, { variant: 'success' }) enqueueSnackbar(`Successfully topped up stamp, your changes will appear soon`, { variant: 'success' })
} catch (error) { } catch (error) {
enqueueSnackbar(`Failed to topup stamp: ${error || 'Unknown reason'}`, { variant: '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 { 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' }) enqueueSnackbar(`Successfully diluted stamp, your changes will appear soon`, { variant: 'success' })
} catch (error) { } catch (error) {
enqueueSnackbar(`Failed to dilute stamp: ${error || 'Unknown reason'}`, { variant: '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<HTMLTextAreaElement | HTMLInputElement>) => { const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
@@ -57,7 +128,7 @@ export default function StampExtensionModal({ type, icon, bee, stamp }: Props):
return ( return (
<Box mb={2}> <Box mb={2}>
<Button variant="contained" onClick={handleClickOpen} startIcon={icon}> <Button className={classes.buttonSelected} variant="contained" onClick={handleClickOpen} startIcon={icon}>
{type} {type}
</Button> </Button>
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title"> <Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
@@ -68,7 +139,7 @@ export default function StampExtensionModal({ type, icon, bee, stamp }: Props):
margin="dense" margin="dense"
id="name" id="name"
type="text" type="text"
placeholder={type === 'Topup' ? 'Amount to add' : 'New depth to dilute'} placeholder={type === StampExtensionType.Topup ? 'Amount to add' : 'New depth to dilute'}
fullWidth fullWidth
value={amount} value={amount}
onChange={handleChange} onChange={handleChange}
@@ -78,7 +149,14 @@ export default function StampExtensionModal({ type, icon, bee, stamp }: Props):
<Button onClick={handleClose} color="primary"> <Button onClick={handleClose} color="primary">
Cancel Cancel
</Button> </Button>
<Button disabled={amount === ''} onClick={handleAction} color="primary"> <Button
disabled={amount === ''}
onClick={async () => {
await handleAction()
handleClose()
}}
color="primary"
>
{type} {type}
</Button> </Button>
</DialogActions> </DialogActions>
+1
View File
@@ -75,6 +75,7 @@ export function SwarmSelect({
value={value} value={value}
className={classes.select} className={classes.select}
displayEmpty displayEmpty
onChange={onChange}
renderValue={(value: unknown) => (value ? renderValue(value) : placeholder)} renderValue={(value: unknown) => (value ? renderValue(value) : placeholder)}
MenuProps={{ MenuListProps: { disablePadding: true }, PaperProps: { square: true } }} MenuProps={{ MenuListProps: { disablePadding: true }, PaperProps: { square: true } }}
> >
+2 -2
View File
@@ -57,7 +57,7 @@ export function SwarmTextInput({
variant="filled" variant="filled"
className={classes.field} className={classes.field}
defaultValue={defaultValue || ''} defaultValue={defaultValue || ''}
InputProps={{ disableUnderline: true }} slotProps={{ input: { disableUnderline: true } }}
placeholder={placeholder} placeholder={placeholder}
/> />
) )
@@ -73,7 +73,7 @@ export function SwarmTextInput({
className={classes.field} className={classes.field}
defaultValue={defaultValue || ''} defaultValue={defaultValue || ''}
onChange={onChange} onChange={onChange}
InputProps={{ disableUnderline: true }} slotProps={{ input: { disableUnderline: true } }}
placeholder={placeholder} placeholder={placeholder}
/> />
) )
+6 -6
View File
@@ -24,14 +24,14 @@ const useStyles = makeStyles()(theme => ({
}, },
}, },
buttonSelected: { buttonSelected: {
color: 'white', color: theme.palette.secondary.main,
backgroundColor: theme.palette.primary.main, backgroundColor: 'white',
'&:hover': { '&:hover': {
color: theme.palette.secondary.main, color: 'white',
backgroundColor: 'white', backgroundColor: theme.palette.primary.main,
'@media (hover: none)': { '@media (hover: none)': {
color: 'white', color: theme.palette.secondary.main,
backgroundColor: theme.palette.primary.main, backgroundColor: 'white',
}, },
}, },
}, },
+2
View File
@@ -12,3 +12,5 @@ export const BEE_DESKTOP_LATEST_RELEASE_PAGE_API =
'https://api.github.com/repos/ethersphere/bee-desktop/releases/latest' 'https://api.github.com/repos/ethersphere/bee-desktop/releases/latest'
export const DEFAULT_BEE_API_HOST = 'http://localhost:1633' export const DEFAULT_BEE_API_HOST = 'http://localhost:1633'
export const DEFAULT_RPC_URL = 'https://xdai.fairdatasociety.org' export const DEFAULT_RPC_URL = 'https://xdai.fairdatasociety.org'
export const MIN_STAMP_DEPTH = 17
export const MAX_STAMP_DEPTH = 255
@@ -179,7 +179,13 @@ export function AdminStatusBar({
const isBusy = loading || isUpgrading || isCreationInProgress const isBusy = loading || isUpgrading || isCreationInProgress
const blurCls = isBusy ? ' is-loading' : '' const blurCls = isBusy ? ' is-loading' : ''
const statusVerb = isCreationInProgress ? 'Creating' : '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.
<br />
This may take a few minutes.
</>
)
const renderModalsAndOverlays = () => { const renderModalsAndOverlays = () => {
return ( return (
@@ -15,7 +15,7 @@ interface ConfirmModalProps {
onCancel?: () => void onCancel?: () => void
showFooter?: boolean showFooter?: boolean
isProgress?: boolean isProgress?: boolean
spinnerMessage?: string spinnerMessage?: React.ReactNode
showMinimize?: boolean showMinimize?: boolean
onMinimize?: () => void onMinimize?: () => void
background?: boolean background?: boolean
@@ -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 { DriveInfo, FileInfo } from '@solarpunkltd/file-manager-lib'
import { ReactElement, useEffect, useMemo, useState } from 'react' import { ReactElement, useEffect, useMemo, useState } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import AlertIcon from 'remixicon-react/AlertLineIcon' import AlertIcon from 'remixicon-react/AlertLineIcon'
import { validateStampStillExists } from '../../utils/bee'
import { getDaysLeft } from '../../utils/common' import { getDaysLeft } from '../../utils/common'
import { Button } from '../Button/Button' import { Button } from '../Button/Button'
import { UpgradeDriveModal } from '../UpgradeDriveModal/UpgradeDriveModal' import { UpgradeDriveModal } from '../UpgradeDriveModal/UpgradeDriveModal'
@@ -16,19 +17,23 @@ import '../../styles/global.scss'
const EXPIRING_ITEMS_PAGE_SIZE = 3 const EXPIRING_ITEMS_PAGE_SIZE = 3
interface ExpiringNotificationModalProps { interface ExpiringNotificationModalProps {
bee: Bee
stamps: PostageBatch[] stamps: PostageBatch[]
drives: DriveInfo[] drives: DriveInfo[]
files: FileInfo[] files: FileInfo[]
onCancelClick: () => void onCancelClick: () => void
setErrorMessage?: (error: string) => void setErrorMessage?: (error: string) => void
setShowError: (show: boolean) => void
} }
export function ExpiringNotificationModal({ export function ExpiringNotificationModal({
bee,
stamps, stamps,
drives, drives,
files, files,
onCancelClick, onCancelClick,
setErrorMessage, setErrorMessage,
setShowError,
}: ExpiringNotificationModalProps): ReactElement { }: ExpiringNotificationModalProps): ReactElement {
const [showUpgradeDriveModal, setShowUpgradeDriveModal] = useState(false) const [showUpgradeDriveModal, setShowUpgradeDriveModal] = useState(false)
const [actualStamp, setActualStamp] = useState<PostageBatch | undefined>(undefined) const [actualStamp, setActualStamp] = useState<PostageBatch | undefined>(undefined)
@@ -75,7 +80,18 @@ export function ExpiringNotificationModal({
files={files} files={files}
currentPage={currentPage} currentPage={currentPage}
index={index} 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) setActualStamp(stamp)
setActualDrive(drive) setActualDrive(drive)
setShowUpgradeDriveModal(true) setShowUpgradeDriveModal(true)
@@ -10,6 +10,7 @@ import React, {
useRef, useRef,
useState, useState,
} from 'react' } from 'react'
import { createPortal } from 'react-dom'
import { useSearch } from '../../../../pages/filemanager/SearchContext' import { useSearch } from '../../../../pages/filemanager/SearchContext'
import { useView } from '../../../../pages/filemanager/ViewContext' import { useView } from '../../../../pages/filemanager/ViewContext'
@@ -87,7 +88,9 @@ function ErrorModalBlock({
return null return null
} }
return <ErrorModal label={label} onClick={onOk} /> const modalRoot = document.querySelector('.fm-main') || document.body
return createPortal(<ErrorModal label={label} onClick={onOk} />, modalRoot)
} }
const extractFilesFromClipboardEvent = (e: React.ClipboardEvent): File[] => { const extractFilesFromClipboardEvent = (e: React.ClipboardEvent): File[] => {
@@ -431,8 +434,10 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
if (rafIdRef.current) { if (rafIdRef.current) {
cancelAnimationFrame(rafIdRef.current) cancelAnimationFrame(rafIdRef.current)
} }
setShowError(false)
} }
}, []) }, [setShowError])
useEffect(() => { useEffect(() => {
let title = currentDrive?.name || '' let title = currentDrive?.name || ''
@@ -3,8 +3,7 @@ import DownIcon from 'remixicon-react/ArrowDownSLineIcon'
import { BulkActionsResult } from '../../../hooks/useBulkActions' import { BulkActionsResult } from '../../../hooks/useBulkActions'
import { SortDir, SortKey } from '../../../hooks/useSorting' import { SortDir, SortKey } from '../../../hooks/useSorting'
import { capitalizeFirstLetter } from '../../../utils/common'
import { capitalizeFirstLetter } from '@/modules/filemanager/utils/common'
interface FileBrowserHeaderProps { interface FileBrowserHeaderProps {
isSearchMode: boolean isSearchMode: boolean
@@ -30,7 +30,7 @@ export function NotificationBar({ setErrorMessage }: NotificationBarProps): Reac
const [stampsToExpire, setStampsToExpire] = useState<PostageBatch[]>([]) const [stampsToExpire, setStampsToExpire] = useState<PostageBatch[]>([])
const [drivesToExpire, setDrivesToExpire] = useState<DriveInfo[]>([]) const [drivesToExpire, setDrivesToExpire] = useState<DriveInfo[]>([])
const { beeApi } = useContext(SettingsContext) const { beeApi } = useContext(SettingsContext)
const { drives, files, adminDrive } = useContext(FMContext) const { drives, files, adminDrive, setShowError } = useContext(FMContext)
const showExpiration = stampsToExpire.length > 0 const showExpiration = stampsToExpire.length > 0
@@ -109,8 +109,9 @@ export function NotificationBar({ setErrorMessage }: NotificationBarProps): Reac
<div className="fm-notification-bar fm-red-font" onClick={() => setShowExpiringModal(true)}> <div className="fm-notification-bar fm-red-font" onClick={() => setShowExpiringModal(true)}>
{stampsToExpire.length} drive{stampsToExpire.length > 1 ? 's' : ''} expiring soon <UpIcon size="16px" /> {stampsToExpire.length} drive{stampsToExpire.length > 1 ? 's' : ''} expiring soon <UpIcon size="16px" />
</div> </div>
{showExpiringModal && ( {showExpiringModal && beeApi && (
<ExpiringNotificationModal <ExpiringNotificationModal
bee={beeApi}
stamps={stampsToExpire} stamps={stampsToExpire}
drives={drivesToExpire} drives={drivesToExpire}
files={files} files={files}
@@ -118,6 +119,7 @@ export function NotificationBar({ setErrorMessage }: NotificationBarProps): Reac
setShowExpiringModal(false) setShowExpiringModal(false)
}} }}
setErrorMessage={setErrorMessage} setErrorMessage={setErrorMessage}
setShowError={setShowError}
/> />
)} )}
</> </>
@@ -3,6 +3,7 @@ import { ReactElement, useState } from 'react'
import CheckDoubleLineIcon from 'remixicon-react/CheckDoubleLineIcon' import CheckDoubleLineIcon from 'remixicon-react/CheckDoubleLineIcon'
import ClipboardIcon from 'remixicon-react/FileCopyLineIcon' import ClipboardIcon from 'remixicon-react/FileCopyLineIcon'
import { uuidV4 } from '../../../../utils'
import { TOOLTIPS } from '../../constants/tooltips' import { TOOLTIPS } from '../../constants/tooltips'
import { getSigner, setSignerPk } from '../../utils/common' import { getSigner, setSignerPk } from '../../utils/common'
import { Button } from '../Button/Button' import { Button } from '../Button/Button'
@@ -10,8 +11,6 @@ import { Tooltip } from '../Tooltip/Tooltip'
import './PrivateKeyModal.scss' import './PrivateKeyModal.scss'
import { uuidV4 } from '@/utils'
type Props = { onSaved: () => void } type Props = { onSaved: () => void }
const generateNewPrivateKey = (): string => { const generateNewPrivateKey = (): string => {
@@ -305,7 +305,11 @@ export function Sidebar({ setErrorMessage, loading }: SidebarProps): ReactElemen
</div> </div>
{isDriveCreationInProgress && ( {isDriveCreationInProgress && (
<div className="fm-sidebar-drive-creation">Creating drive, please do not reload</div> <div className="fm-sidebar-drive-creation">
Creating drive please do not reload the page.
<br />
This may take a few minutes.
</div>
)} )}
</div> </div>
) )
@@ -26,7 +26,7 @@ interface UseFileFilteringReturn {
export function useFileFiltering(props: UseFileFilteringProps): UseFileFilteringReturn { export function useFileFiltering(props: UseFileFilteringProps): UseFileFilteringReturn {
const { files, currentDrive, view, isSearchMode, query, scope, includeActive, includeTrashed } = props 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( const statusIncluded = useCallback(
(fi: FileInfo): boolean => { (fi: FileInfo): boolean => {
@@ -44,9 +44,11 @@ export function useFileFiltering(props: UseFileFilteringProps): UseFileFiltering
const matchesQuery = useCallback( const matchesQuery = useCallback(
(fi: FileInfo): boolean => { (fi: FileInfo): boolean => {
if (!q) return true if (!q) return true
const name = fi.name.toLowerCase() const name = fi.name.toLowerCase().normalize('NFC')
const mime = (fi.customMetadata?.mime || '').toLowerCase() const mime = (fi.customMetadata?.mime || '').toLowerCase().normalize('NFC')
const topic = String(fi.topic ?? '').toLowerCase() const topic = String(fi.topic ?? '')
.toLowerCase()
.normalize('NFC')
return name.includes(q) || mime.includes(q) || topic.includes(q) return name.includes(q) || mime.includes(q) || topic.includes(q)
}, },
@@ -2,7 +2,7 @@ import { ReactElement } from 'react'
import FileIcon from 'remixicon-react/FileTextLineIcon' import FileIcon from 'remixicon-react/FileTextLineIcon'
import ImageIcon from 'remixicon-react/Image2LineIcon' import ImageIcon from 'remixicon-react/Image2LineIcon'
import { guessMime } from './view' import { guessMime } from '../../../utils/file'
interface ContextMenuProps { interface ContextMenuProps {
name: string name: string
+1 -1
View File
@@ -1,10 +1,10 @@
import { FileInfo, FileManager } from '@solarpunkltd/file-manager-lib' import { FileInfo, FileManager } from '@solarpunkltd/file-manager-lib'
import { guessMime, VIEWERS } from '../../../utils/file'
import { DownloadProgress, DownloadState } from '../constants/transfers' import { DownloadProgress, DownloadState } from '../constants/transfers'
import { AbortManager } from './abortManager' import { AbortManager } from './abortManager'
import { isDirectoryPickerSupported, isPickerSupported } from './fileOperations' import { isDirectoryPickerSupported, isPickerSupported } from './fileOperations'
import { guessMime, VIEWERS } from './view'
const DefaultDownloadFolder = 'downloads' const DefaultDownloadFolder = 'downloads'
-114
View File
@@ -1,114 +0,0 @@
const EXT_TO_MIME: Record<string, string> = {
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<string, string> | 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) =>
`<html><head><meta charset="utf-8"/><title>${title}</title></head><body style="margin:0;background:#000">
<video controls autoplay style="width:100%;height:100%" src="${u}"></video>
</body></html>`
const AUDIO_HTML = (u: string, title: string) =>
`<html><head><meta charset="utf-8"/><title>${title}</title></head><body>
<audio controls autoplay style="width:100%" src="${u}"></audio>
</body></html>`
const IMAGE_HTML = (u: string, title: string) =>
`<html><head><meta charset="utf-8"/><title>${title}</title></head><body style="margin:0;background:#111;display:grid;place-items:center;min-height:100vh">
<img style="max-width:100%;max-height:100vh" src="${u}" />
</body></html>`
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
},
},
]
+1 -1
View File
@@ -104,7 +104,7 @@ export function AccountFeeds(): ReactElement {
{x.feedHash && <ExpandableListItemKey label="Feed hash" value={x.feedHash} />} {x.feedHash && <ExpandableListItemKey label="Feed hash" value={x.feedHash} />}
<Box mt={0.75}> <Box mt={0.75}>
<ExpandableListItemActions> <ExpandableListItemActions>
<SwarmButton onClick={() => viewFeed(x.uuid)} iconType={Info}> <SwarmButton onClick={() => viewFeed(x.uuid)} iconType={Info} disabled={Boolean(!x.feedHash)}>
View Feed Page View Feed Page
</SwarmButton> </SwarmButton>
<SwarmButton onClick={() => onShowExport(x)} iconType={Download}> <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 { Box, Grid, Typography } from '@mui/material'
import { Wallet } from 'ethers'
import { Form, Formik } from 'formik' import { Form, Formik } from 'formik'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useState } from 'react' import { ReactElement, useContext, useState } from 'react'
@@ -12,7 +13,7 @@ import ExpandableListItemActions from '../../components/ExpandableListItemAction
import ExpandableListItemKey from '../../components/ExpandableListItemKey' import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import { HistoryHeader } from '../../components/HistoryHeader' import { HistoryHeader } from '../../components/HistoryHeader'
import { SwarmButton } from '../../components/SwarmButton' import { SwarmButton } from '../../components/SwarmButton'
import { SwarmSelect } from '../../components/SwarmSelect' import { SelectEvent, SwarmSelect } from '../../components/SwarmSelect'
import { SwarmTextInput } from '../../components/SwarmTextInput' import { SwarmTextInput } from '../../components/SwarmTextInput'
import { Context as FeedsContext, IdentityType } from '../../providers/Feeds' import { Context as FeedsContext, IdentityType } from '../../providers/Feeds'
import { Context as SettingsContext } from '../../providers/Settings' import { Context as SettingsContext } from '../../providers/Settings'
@@ -34,7 +35,8 @@ const initialValues: FormValues = {
export default function CreateNewFeed(): ReactElement { export default function CreateNewFeed(): ReactElement {
const { beeApi } = useContext(SettingsContext) const { beeApi } = useContext(SettingsContext)
const { identities, setIdentities } = useContext(FeedsContext) 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 { enqueueSnackbar } = useSnackbar()
const navigate = useNavigate() const navigate = useNavigate()
@@ -48,11 +50,24 @@ export default function CreateNewFeed(): ReactElement {
return 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) { if (!stamps || !stamps.length) {
enqueueSnackbar(<span>No stamp available</span>, { variant: 'error' }) enqueueSnackbar(<span>No usable stamp available</span>, { variant: 'error' })
setLoading(false) setLoading(false)
return return
@@ -65,17 +80,29 @@ export default function CreateNewFeed(): ReactElement {
return return
} }
const identity = await convertWalletToIdentity(wallet, values.type, values.identityName, values.password) try {
persistIdentity(identities, identity) const identity = await convertWalletToIdentity(wallet, values.type, values.identityName, values.password)
setIdentities(identities) persistIdentity(identities, identity)
navigate(ROUTES.ACCOUNT_FEEDS) setIdentities(identities)
setLoading(false) 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() { function cancel() {
navigate(-1) navigate(-1)
} }
function onIdentityTypeChange(event: SelectEvent) {
const type = event.target.value as IdentityType
setIdentityType(type)
}
return ( return (
<div> <div>
<HistoryHeader>Create new feed</HistoryHeader> <HistoryHeader>Create new feed</HistoryHeader>
@@ -102,10 +129,13 @@ export default function CreateNewFeed(): ReactElement {
<SwarmSelect <SwarmSelect
formik formik
name="type" name="type"
label={'type'}
value={identityType}
options={[ options={[
{ label: 'Keypair Only', value: IdentityType.PrivateKey }, { label: 'Keypair Only', value: IdentityType.PrivateKey },
{ label: 'Password Protected', value: IdentityType.V3 }, { label: 'Password Protected', value: IdentityType.V3 },
]} ]}
onChange={onIdentityTypeChange}
/> />
</Box> </Box>
{values.type === IdentityType.V3 && <SwarmTextInput name="password" label="Password" password formik />} {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 { status } = useContext(BeeContext)
const { hash } = useParams() const { hash } = useParams()
const [selectedStamp, setSelectedStamp] = useState<string | null>(null) const [selectedStamp, setSelectedStamp] = useState<string | null>(stamps ? stamps[0]?.batchID.toHex() : null)
const [selectedIdentity, setSelectedIdentity] = useState<Identity | null>(null) const [selectedIdentity, setSelectedIdentity] = useState<Identity | null>(identities[0] ?? null)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
const [showPasswordPrompt, setShowPasswordPrompt] = useState(false) const [showPasswordPrompt, setShowPasswordPrompt] = useState(false)
@@ -119,19 +119,28 @@ export default function UpdateFeed(): ReactElement {
<HistoryHeader>Update feed</HistoryHeader> <HistoryHeader>Update feed</HistoryHeader>
<Box mb={2}> <Box mb={2}>
<Grid container> <Grid container>
<SwarmSelect {identities && identities.length ? (
options={identities.map(x => ({ value: x.uuid, label: `${x.name} Website` }))} <SwarmSelect
onChange={onFeedChange} value={selectedIdentity?.uuid ?? ''}
label="Feed" 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> </Grid>
</Box> </Box>
<Box mb={4}> <Box mb={4}>
<Grid container> <Grid container>
{stamps ? ( {stamps && stamps.length ? (
<SwarmSelect <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} onChange={onStampChange}
label="Stamp" label="Stamp"
/> />
+1 -1
View File
@@ -104,7 +104,7 @@ export default function Feeds(): ReactElement {
{x.feedHash && <ExpandableListItemKey label="Feed hash" value={x.feedHash} />} {x.feedHash && <ExpandableListItemKey label="Feed hash" value={x.feedHash} />}
<Box mt={0.75}> <Box mt={0.75}>
<ExpandableListItemActions> <ExpandableListItemActions>
<SwarmButton onClick={() => viewFeed(x.uuid)} iconType={Info}> <SwarmButton onClick={() => viewFeed(x.uuid)} iconType={Info} disabled={Boolean(!x.feedHash)}>
View Feed Page View Feed Page
</SwarmButton> </SwarmButton>
<SwarmButton onClick={() => onShowExport(x)} iconType={Download}> <SwarmButton onClick={() => onShowExport(x)} iconType={Download}>
+74 -23
View File
@@ -1,26 +1,27 @@
import { BeeModes } from '@ethersphere/bee-js'
import { DriveInfo, FileManagerBase } from '@solarpunkltd/file-manager-lib' import { DriveInfo, FileManagerBase } from '@solarpunkltd/file-manager-lib'
import { ReactElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' 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 { SearchProvider } from './SearchContext'
import { ViewProvider } from './ViewContext' import { ViewProvider } from './ViewContext'
import './FileManager.scss' 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 }) { function PrivateKeyModalBlock({ onSaved }: { onSaved: () => void }) {
return ( return (
<div className="fm-main"> <div className="fm-main">
@@ -44,6 +45,18 @@ function InitializationErrorBlock({ onOk }: { onOk: () => void }) {
) )
} }
function UltraLightNodeErrorBlock() {
return (
<div className="fm-main">
<div className="fm-loading">
<div className="fm-loading-title">
File Manager is not available with an Ultra-light node. Please upgrade to a Light node to continue.
</div>
</div>
</div>
)
}
function ResetModalBlock({ cacheHelpUrl, onConfirm }: { cacheHelpUrl: string; onConfirm: () => void }) { function ResetModalBlock({ cacheHelpUrl, onConfirm }: { cacheHelpUrl: string; onConfirm: () => void }) {
return ( return (
<div className="fm-main"> <div className="fm-main">
@@ -95,6 +108,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 }) { function ErrorModalBlock({ onClick, label }: { onClick: () => void; label: string }) {
return <ErrorModal label={label} onClick={onClick} /> return <ErrorModal label={label} onClick={onClick} />
} }
@@ -153,10 +182,12 @@ function FileManagerMainContent(props: {
enum PageState { enum PageState {
Connecting = 'connecting', // still warming up — show nothing / loader 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 NoPrivateKey = 'no-pk', // private key not set
Loading = 'loading', // bee ready, pk present, FM init in progress Loading = 'loading', // bee ready, pk present, FM init in progress
Reset = 'reset', // STATE_INVALID emitted and user has not yet acknowledged Reset = 'reset', // STATE_INVALID emitted and user has not yet acknowledged
InitError = 'init-error', // FM init completed with an error (non-reset case) 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 Initial = 'initial', // FM ready but no admin stamp/drive → show InitialModal
AdminError = 'admin-error', // drive creation failed AdminError = 'admin-error', // drive creation failed
Ready = 'ready', // fully operational Ready = 'ready', // fully operational
@@ -172,7 +203,7 @@ export function FileManagerPage(): ReactElement {
const [connectionErrorDismissed, setConnectionErrorDismissed] = useState<boolean>(false) const [connectionErrorDismissed, setConnectionErrorDismissed] = useState<boolean>(false)
const [cacheHelpUrl, setCacheHelpUrl] = useState<string>(cacheClearUrls[BrowserPlatform.Chrome]) const [cacheHelpUrl, setCacheHelpUrl] = useState<string>(cacheClearUrls[BrowserPlatform.Chrome])
const { status } = useContext(BeeContext) const { status, chainState, nodeInfo } = useContext(BeeContext)
const { fm, initDone, shallReset, adminDrive, initializationError, notifyPkSaved } = useContext(FMContext) const { fm, initDone, shallReset, adminDrive, initializationError, notifyPkSaved } = useContext(FMContext)
useEffect(() => { useEffect(() => {
@@ -207,8 +238,12 @@ export function FileManagerPage(): ReactElement {
}, [isConnectionError]) }, [isConnectionError])
const pageState = useMemo((): PageState => { const pageState = useMemo((): PageState => {
const isChainSyncing = chainState === null
if (!isBeeReady && !initDone) return PageState.Connecting if (!isBeeReady && !initDone) return PageState.Connecting
if (nodeInfo?.beeMode === BeeModes.ULTRA_LIGHT) return PageState.UltraLightNode
if (!hasPk) return PageState.NoPrivateKey if (!hasPk) return PageState.NoPrivateKey
if (!initDone) return PageState.Loading if (!initDone) return PageState.Loading
@@ -217,12 +252,15 @@ export function FileManagerPage(): ReactElement {
if (initializationError && !shallReset) return PageState.InitError if (initializationError && !shallReset) return PageState.InitError
if (showAdminErrorModal) return PageState.AdminError
const hasAdminStamp = Boolean(fm?.adminStamp) const hasAdminStamp = Boolean(fm?.adminStamp)
const hasAdminDrive = Boolean(adminDrive) 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 return PageState.Ready
}, [ }, [
@@ -236,6 +274,8 @@ export function FileManagerPage(): ReactElement {
fm, fm,
adminDrive, adminDrive,
isCreationInProgress, isCreationInProgress,
chainState,
nodeInfo?.beeMode,
]) ])
const handlePrivateKeySaved = useCallback(() => { const handlePrivateKeySaved = useCallback(() => {
@@ -251,10 +291,18 @@ export function FileManagerPage(): ReactElement {
const loading = !fm?.adminStamp || !adminDrive const loading = !fm?.adminStamp || !adminDrive
const isFormbricksActive = Boolean(fm && fm.adminStamp && adminDrive && !loading) const isFormbricksActive = Boolean(fm && fm.adminStamp && adminDrive && !loading)
if (pageState === PageState.UltraLightNode) {
return <UltraLightNodeErrorBlock />
}
if (pageState === PageState.Connecting || pageState === PageState.Loading) { if (pageState === PageState.Connecting || pageState === PageState.Loading) {
return <LoadingBlock /> return <LoadingBlock />
} }
if (pageState === PageState.ChainSyncing) {
return <ChainSyncingBlock />
}
if (pageState === PageState.NoPrivateKey) { if (pageState === PageState.NoPrivateKey) {
return <PrivateKeyModalBlock onSaved={handlePrivateKeySaved} /> return <PrivateKeyModalBlock onSaved={handlePrivateKeySaved} />
} }
@@ -289,12 +337,15 @@ export function FileManagerPage(): ReactElement {
} }
if (pageState === PageState.AdminError) { 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 ( return (
<ErrorModalBlock <ErrorModalBlock
label={ label={adminErrorLabel}
errorMessage ||
'Error creating Admin Drive. Please try again. Possible causes include insufficient xDAI balance or a lost connection to the RPC.'
}
onClick={() => { onClick={() => {
setAdminShowErrorModal(false) setAdminShowErrorModal(false)
setErrorMessage('') setErrorMessage('')
+20 -8
View File
@@ -8,7 +8,7 @@ import { FitAudio } from '../../components/FitAudio'
import { FitImage } from '../../components/FitImage' import { FitImage } from '../../components/FitImage'
import { FitVideo } from '../../components/FitVideo' import { FitVideo } from '../../components/FitVideo'
import { shortenText } from '../../utils' import { shortenText } from '../../utils'
import { getHumanReadableFileSize } from '../../utils/file' import { getHumanReadableFileSize, guessMime } from '../../utils/file'
import { shortenHash } from '../../utils/hash' import { shortenHash } from '../../utils/hash'
import { AssetIcon } from './AssetIcon' import { AssetIcon } from './AssetIcon'
@@ -18,16 +18,20 @@ interface Props {
metadata?: Metadata metadata?: Metadata
} }
const getPreviewElement = (previewUri?: string, metadata?: Metadata) => { const getPreviewElement = (previewUri?: string, metadata?: Metadata, type?: string) => {
if (metadata?.isVideo) { 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" /> return <FitVideo src={previewUri} maxWidth="250px" maxHeight="175px" />
} }
if (metadata?.isAudio) { if (metadata?.isAudio || isAudioType) {
return <FitAudio src={previewUri} maxWidth="250px" /> return <FitAudio src={previewUri} maxWidth="250px" />
} }
if (metadata?.isImage) { if (metadata?.isImage || isImageType) {
return <FitImage maxWidth="250px" maxHeight="175px" alt="Upload Preview" src={previewUri} /> 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 />} /> return <AssetIcon icon={<File />} />
} }
const getType = (metadata?: Metadata) => { export const getType = (metadata?: Metadata): string => {
if (metadata?.isWebsite) return 'Website' if (metadata?.isWebsite) return 'Website'
if (metadata?.type === 'folder') return 'Folder' 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) // TODO: add optional prop for indexDocument when it is already known (e.g. downloading a manifest)
export function AssetPreview({ metadata, previewUri }: Props): ReactElement | null { export function AssetPreview({ metadata, previewUri }: Props): ReactElement | null {
const previewElement = useMemo(() => getPreviewElement(previewUri, metadata), [metadata, previewUri])
const type = useMemo(() => getType(metadata), [metadata]) const type = useMemo(() => getType(metadata), [metadata])
const previewElement = useMemo(() => getPreviewElement(previewUri, metadata, type), [metadata, type, previewUri])
return ( return (
<Box mb={4}> <Box mb={4}>
+3 -3
View File
@@ -16,7 +16,7 @@ import { ROUTES } from '../../routes'
import { determineHistoryName, LocalStorageKeys, putHistory } from '../../utils/localStorage' import { determineHistoryName, LocalStorageKeys, putHistory } from '../../utils/localStorage'
import { loadManifest } from '../../utils/manifest' import { loadManifest } from '../../utils/manifest'
import { AssetPreview } from './AssetPreview' import { AssetPreview, getType } from './AssetPreview'
import { AssetSummary } from './AssetSummary' import { AssetSummary } from './AssetSummary'
import { AssetSyncing } from './AssetSyncing' import { AssetSyncing } from './AssetSyncing'
import { DownloadActionBar } from './DownloadActionBar' import { DownloadActionBar } from './DownloadActionBar'
@@ -46,7 +46,7 @@ export function Share(): ReactElement {
const count = Object.keys(entries).length const count = Object.keys(entries).length
const isVideo = Boolean(indexDocument && /.*\.(mp4|webm|ogv)$/i.test(indexDocument)) 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 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) { if (isImage || isVideo || isAudio) {
setPreview(`${apiUrl}/bzz/${hash}`) setPreview(`${apiUrl}/bzz/${hash}`)
@@ -54,7 +54,7 @@ export function Share(): ReactElement {
setMetadata({ setMetadata({
hash, hash,
type: count > 1 ? 'folder' : 'unknown', type: count > 1 ? 'folder' : getType(),
name: indexDocument || hash || '', name: indexDocument || hash || '',
count, count,
isWebsite: Boolean(indexDocument && /.*\.html?$/i.test(indexDocument)), 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 SettingsContext } from '../../providers/Settings'
import { Context as StampsContext, EnrichedPostageBatch } from '../../providers/Stamps' import { Context as StampsContext, EnrichedPostageBatch } from '../../providers/Stamps'
import { ROUTES } from '../../routes' import { ROUTES } from '../../routes'
import { waitUntilStampUsable } from '../../utils'
import { detectIndexHtml, getAssetNameFromFiles, packageFile } from '../../utils/file' import { detectIndexHtml, getAssetNameFromFiles, packageFile } from '../../utils/file'
import { persistIdentity, updateFeed } from '../../utils/identity' import { persistIdentity, updateFeed } from '../../utils/identity'
import { LocalStorageKeys, putHistory } from '../../utils/localStorage' import { LocalStorageKeys, putHistory } from '../../utils/localStorage'
import { waitUntilStampUsable } from '../../utils/stamp'
import { FeedPasswordDialog } from '../feeds/FeedPasswordDialog' import { FeedPasswordDialog } from '../feeds/FeedPasswordDialog'
import { PostageStampAdvancedCreation } from '../stamps/PostageStampAdvancedCreation' import { PostageStampAdvancedCreation } from '../stamps/PostageStampAdvancedCreation'
import { PostageStampSelector } from '../stamps/PostageStampSelector' import { PostageStampSelector } from '../stamps/PostageStampSelector'
+2 -1
View File
@@ -1,3 +1,4 @@
import { BeeModes } from '@ethersphere/bee-js'
import { useContext } from 'react' import { useContext } from 'react'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
import Upload from 'remixicon-react/UploadLineIcon' import Upload from 'remixicon-react/UploadLineIcon'
@@ -19,7 +20,7 @@ export function WalletInfoCard() {
)} xBZZ | ${walletBalance.nativeTokenBalance.toSignificantDigits(4)} xDAI` )} 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 ( return (
<Card <Card
buttonProps={{ buttonProps={{
+4 -1
View File
@@ -15,7 +15,7 @@ import NodeInfoCard from './NodeInfoCard'
import { WalletInfoCard } from './WalletInfoCard' import { WalletInfoCard } from './WalletInfoCard'
export default function Status(): ReactElement { export default function Status(): ReactElement {
const { beeVersion, status, topology, nodeInfo, walletBalance } = useContext(BeeContext) const { beeVersion, status, topology, nodeInfo, nodeStatus, walletBalance } = useContext(BeeContext)
const { isDesktop, desktopUrl } = useContext(SettingsContext) const { isDesktop, desktopUrl } = useContext(SettingsContext)
const { beeDesktopVersion } = useBeeDesktop(isDesktop, desktopUrl) const { beeDesktopVersion } = useBeeDesktop(isDesktop, desktopUrl)
const { newBeeDesktopVersion } = useNewBeeDesktopVersion(isDesktop, desktopUrl, false) const { newBeeDesktopVersion } = useNewBeeDesktopVersion(isDesktop, desktopUrl, false)
@@ -40,7 +40,10 @@ export default function Status(): ReactElement {
<div style={{ height: '2px' }} /> <div style={{ height: '2px' }} />
<ExpandableListItem label="Connected peers" value={topology?.connected ?? '-'} /> <ExpandableListItem label="Connected peers" value={topology?.connected ?? '-'} />
<ExpandableListItem label="Population" value={topology?.population ?? '-'} /> <ExpandableListItem label="Population" value={topology?.population ?? '-'} />
<ExpandableListItem label="Pullsync rate" value={nodeStatus?.pullsyncRate} />
<ExpandableListItem label="Depth" value={topology?.depth ?? '-'} /> <ExpandableListItem label="Depth" value={topology?.depth ?? '-'} />
<ExpandableListItem label="Neighborhood size" value={nodeStatus?.neighborhoodSize} />
<ExpandableListItem label="Node is reachable" value={nodeStatus?.isReachable?.toString()} />
<ChainSync /> <ChainSync />
<div style={{ height: '16px' }} /> <div style={{ height: '16px' }} />
+26
View File
@@ -0,0 +1,26 @@
import CircularProgress from '@mui/material/CircularProgress'
import { ReactElement, useContext } from 'react'
import ExpandableList from '../../components/ExpandableList'
import { Redistribution } from '../../components/Redistribution'
import { Context as SettingsContext } from '../../providers/Settings'
export default function RedistributionPage(): ReactElement {
const { isLoading } = useContext(SettingsContext)
if (isLoading) {
return (
<div style={{ textAlign: 'center', width: '100%' }}>
<CircularProgress />
</div>
)
}
return (
<>
<ExpandableList label="Redistribution" defaultOpen>
<Redistribution />
</ExpandableList>
</>
)
}
@@ -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 { Box, Grid, IconButton, Typography } from '@mui/material'
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
@@ -11,12 +11,14 @@ import { makeStyles } from 'tss-react/mui'
import { SwarmButton } from '../../components/SwarmButton' import { SwarmButton } from '../../components/SwarmButton'
import { SwarmSelect } from '../../components/SwarmSelect' import { SwarmSelect } from '../../components/SwarmSelect'
import { SwarmTextInput } from '../../components/SwarmTextInput' import { SwarmTextInput } from '../../components/SwarmTextInput'
import { MAX_STAMP_DEPTH, MIN_STAMP_DEPTH } from '../../constants'
import { Context as BeeContext } from '../../providers/Bee' import { Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings' import { Context as SettingsContext } from '../../providers/Settings'
import { Context as StampsContext } from '../../providers/Stamps' import { Context as StampsContext } from '../../providers/Stamps'
import { ROUTES } from '../../routes' import { ROUTES } from '../../routes'
import { secondsToTimeString } from '../../utils' import { secondsToTimeString } from '../../utils'
import { getHumanReadableFileSize } from '../../utils/file' import { getHumanReadableFileSize } from '../../utils/file'
import { validateDepthInput } from '../../utils/stamp'
interface Props { interface Props {
onFinished: () => void onFinished: () => void
@@ -80,7 +82,7 @@ export function PostageStampAdvancedCreation({ onFinished }: Props): ReactElemen
} }
function getPrice(depth: number, amount: bigint): string { 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) { if (hasInvalidInput) {
return '-' return '-'
@@ -147,38 +149,15 @@ export function PostageStampAdvancedCreation({ onFinished }: Props): ReactElemen
setAmountInput(validAmountInput) 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() { function renderStampVolumesInfo() {
const depth = parseInt(depthInput, 10) 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 '-' return '-'
} }
const theoreticalMaximumVolume = getHumanReadableFileSize(Utils.getStampTheoreticalBytes(depth)) const theoreticalMaximumVolume = getHumanReadableFileSize(Utils.getStampTheoreticalBytes(depth))
const effectiveVolume = getHumanReadableFileSize(Utils.getStampEffectiveBytes(depth)) const effectiveVolume = getHumanReadableFileSize(Utils.getStampEffectiveBytes(depth, false, RedundancyLevel.OFF))
return ( return (
<Grid container alignItems="center" className={classes.stampVolumeWrapper}> <Grid container alignItems="center" className={classes.stampVolumeWrapper}>
@@ -217,7 +196,11 @@ export function PostageStampAdvancedCreation({ onFinished }: Props): ReactElemen
</Typography> </Typography>
</Box> </Box>
<Box mb={2}> <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}> <Box mt={0.25} sx={{ bgcolor: '#f6f6f6' }} p={2}>
<Grid container justifyContent="space-between" alignItems="center"> <Grid container justifyContent="space-between" alignItems="center">
<Typography>Corresponding file size</Typography> <Typography>Corresponding file size</Typography>
@@ -242,7 +225,7 @@ export function PostageStampAdvancedCreation({ onFinished }: Props): ReactElemen
<Box mb={2}> <Box mb={2}>
<SwarmSelect <SwarmSelect
label="Immutable" label="Immutable"
value="No" value={immutable ? 'Yes' : 'No'}
onChange={event => setImmutable(event.target.value === 'Yes')} onChange={event => setImmutable(event.target.value === 'Yes')}
options={[ options={[
{ value: 'Yes', label: 'Yes' }, { 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 { Box, Button, Grid, Slider, Typography } from '@mui/material'
import { useSnackbar } from 'notistack' import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useState } from 'react' import { ReactElement, useContext, useState } from 'react'
@@ -8,10 +8,12 @@ import { makeStyles } from 'tss-react/mui'
import { SwarmButton } from '../../components/SwarmButton' import { SwarmButton } from '../../components/SwarmButton'
import { SwarmTextInput } from '../../components/SwarmTextInput' import { SwarmTextInput } from '../../components/SwarmTextInput'
import { Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings' import { Context as SettingsContext } from '../../providers/Settings'
import { Context as StampsContext } from '../../providers/Stamps' import { Context as StampsContext } from '../../providers/Stamps'
import { ROUTES } from '../../routes' import { ROUTES } from '../../routes'
import { secondsToTimeString } from '../../utils' import { secondsToTimeString } from '../../utils'
import { validateDepthInput } from '../../utils/stamp'
interface Props { interface Props {
onFinished: () => void onFinished: () => void
@@ -48,12 +50,17 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen
const { classes } = useStyles() const { classes } = useStyles()
const { refresh } = useContext(StampsContext) const { refresh } = useContext(StampsContext)
const { beeApi } = useContext(SettingsContext) const { beeApi } = useContext(SettingsContext)
const { chainState } = useContext(BeeContext)
const [depthInput, setDepthInput] = useState<number>(Utils.getDepthForSize(Size.fromGigabytes(4))) const [depthInput, setDepthInput] = useState<number>(Utils.getDepthForSize(Size.fromGigabytes(4)))
const [amountInput, setAmountInput] = useState<bigint>(Utils.getAmountForDuration(Duration.fromDays(30), 26500, 5)) const [amountInput, setAmountInput] = useState<bigint>(Utils.getAmountForDuration(Duration.fromDays(30), 26500, 5))
const [labelInput, setLabelInput] = useState('') const [labelInput, setLabelInput] = useState('')
const [submitting, setSubmitting] = useState(false) const [submitting, setSubmitting] = useState(false)
const [buttonValue, setButtonValue] = useState(4) 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) => { const getBatchValue = (value: number) => {
return ( return (
@@ -74,18 +81,18 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen
if (typeof newValue !== 'number') { if (typeof newValue !== 'number') {
return return
} }
const amountValue = Utils.getAmountForDuration(Duration.fromDays(newValue), 26500, 5) const amountValue = Utils.getAmountForDuration(Duration.fromDays(newValue), currentPrice, 5)
setAmountInput(amountValue) setAmountInput(amountValue)
setSliderValue(newValue)
} }
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
function getTtl(amount: bigint): string { function getTtl(amount: bigint): string {
const pricePerBlock = 24000
return `${secondsToTimeString( return `${secondsToTimeString(
Utils.getStampDuration(amount, pricePerBlock, 5).toSeconds(), Utils.getStampDuration(amount, currentPrice, 5).toSeconds(),
)} (with price of ${pricePerBlock} PLUR per block)` )} (with price of ${currentPrice} PLUR per block)`
} }
function getPrice(depth: number, amount: bigint): string { function getPrice(depth: number, amount: bigint): string {
@@ -106,15 +113,15 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen
} }
setSubmitting(true) 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() await refresh()
onFinished() onFinished()
} catch (e) { } catch (e) {
@@ -127,8 +134,8 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen
function handleBatchSize(gigabytes: number) { function handleBatchSize(gigabytes: number) {
setButtonValue(gigabytes) setButtonValue(gigabytes)
const capacity = Utils.getDepthForSize(Size.fromGigabytes(gigabytes)) const capacity = Utils.getDepthForSize(Size.fromGigabytes(gigabytes), false, RedundancyLevel.OFF)
setDepthInput(capacity) validateDepthInput(String(capacity), setDepthError, (v: string) => setDepthInput(Number(v)))
} }
return ( return (
@@ -162,6 +169,7 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen
{getBatchValue(32)} {getBatchValue(32)}
{getBatchValue(256)} {getBatchValue(256)}
</Box> </Box>
{depthError && <Typography>{depthError}</Typography>}
</Box> </Box>
<Box mb={1}> <Box mb={1}>
<Typography variant="h2">Data persistence</Typography> <Typography variant="h2">Data persistence</Typography>
@@ -187,7 +195,7 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen
</Box> </Box>
<Box display="flex" justifyContent={'right'} mt={0.5}> <Box display="flex" justifyContent={'right'} mt={0.5}>
<Typography style={{ fontSize: '10px', color: 'rgba(0, 0, 0, 0.26)' }}> <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> </Typography>
</Box> </Box>
</Box> </Box>
@@ -200,7 +208,7 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen
<Grid container justifyContent="space-between" alignItems="center"> <Grid container justifyContent="space-between" alignItems="center">
<Grid> <Grid>
<SwarmButton <SwarmButton
disabled={submitting || !depthInput || !amountInput} disabled={submitting || !depthInput || Boolean(depthError) || !amountInput}
onClick={submit} onClick={submit}
iconType={Check} iconType={Check}
loading={submitting} loading={submitting}
+13 -9
View File
@@ -7,8 +7,9 @@ import ExpandableList from '../../components/ExpandableList'
import ExpandableListItem from '../../components/ExpandableListItem' import ExpandableListItem from '../../components/ExpandableListItem'
import ExpandableListItemActions from '../../components/ExpandableListItemActions' import ExpandableListItemActions from '../../components/ExpandableListItemActions'
import ExpandableListItemKey from '../../components/ExpandableListItemKey' import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import StampExtensionModal from '../../components/StampExtensionModal' import StampExtensionModal, { StampExtensionType } from '../../components/StampExtensionModal'
import { Context } from '../../providers/Settings' import { Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings'
import { EnrichedPostageBatch } from '../../providers/Stamps' import { EnrichedPostageBatch } from '../../providers/Stamps'
import { secondsToTimeString } from '../../utils' import { secondsToTimeString } from '../../utils'
import { getHumanReadableFileSize } from '../../utils/file' import { getHumanReadableFileSize } from '../../utils/file'
@@ -20,7 +21,8 @@ interface Props {
} }
function StampsTable({ postageStamps }: Props): ReactElement | null { function StampsTable({ postageStamps }: Props): ReactElement | null {
const { beeApi } = useContext(Context) const { beeApi } = useContext(SettingsContext)
const { status } = useContext(BeeContext)
if (!postageStamps || !beeApi) { if (!postageStamps || !beeApi) {
return null return null
@@ -37,8 +39,8 @@ function StampsTable({ postageStamps }: Props): ReactElement | null {
<ExpandableListItem label="Depth" value={String(stamp.depth)} /> <ExpandableListItem label="Depth" value={String(stamp.depth)} />
<ExpandableListItem <ExpandableListItem
label="Capacity" label="Capacity"
value={`${getHumanReadableFileSize(2 ** stamp.depth * 4096 * stamp.usage)} / ${getHumanReadableFileSize( value={`${getHumanReadableFileSize(stamp.size.toBytes() - stamp.remainingSize.toBytes())} / ${getHumanReadableFileSize(
2 ** stamp.depth * 4096, stamp.size.toBytes(),
)}`} )}`}
/> />
<ExpandableListItem label="Amount" value={parseInt(stamp.amount, 10).toLocaleString()} /> <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} /> <ExpandableListItem label="Purchase Block Number" value={stamp.blockNumber} />
<ExpandableListItemActions> <ExpandableListItemActions>
<StampExtensionModal <StampExtensionModal
type="Topup" type={StampExtensionType.Topup}
icon={<TimerFlashFill size="1rem" />} icon={<TimerFlashFill size="1rem" />}
bee={beeApi} bee={beeApi}
stamp={stamp.batchID} stamp={stamp}
status={status.all}
/> />
<StampExtensionModal <StampExtensionModal
type="Dilute" type={StampExtensionType.Dilute}
icon={<TimerFlashLine size="1rem" />} icon={<TimerFlashLine size="1rem" />}
bee={beeApi} bee={beeApi}
stamp={stamp.batchID} stamp={stamp}
status={status.all}
/> />
</ExpandableListItemActions> </ExpandableListItemActions>
</> </>
+18
View File
@@ -5,11 +5,13 @@ import {
ChainState, ChainState,
ChequebookAddressResponse, ChequebookAddressResponse,
ChequebookBalanceResponse, ChequebookBalanceResponse,
DebugStatus,
LastChequesResponse, LastChequesResponse,
NodeAddresses, NodeAddresses,
NodeInfo, NodeInfo,
Peer, Peer,
PeerBalance, PeerBalance,
RedistributionState,
Topology, Topology,
WalletBalance, WalletBalance,
} from '@ethersphere/bee-js' } from '@ethersphere/bee-js'
@@ -61,6 +63,7 @@ interface ContextInterface {
apiHealth: boolean apiHealth: boolean
nodeAddresses: NodeAddresses | null nodeAddresses: NodeAddresses | null
nodeInfo: NodeInfo | null nodeInfo: NodeInfo | null
nodeStatus: DebugStatus | null
topology: Topology | null topology: Topology | null
chequebookAddress: ChequebookAddressResponse | null chequebookAddress: ChequebookAddressResponse | null
peers: Peer[] | null peers: Peer[] | null
@@ -71,6 +74,7 @@ interface ContextInterface {
settlements: AllSettlements | null settlements: AllSettlements | null
chainState: ChainState | null chainState: ChainState | null
walletBalance: WalletBalance | null walletBalance: WalletBalance | null
redistributionState: RedistributionState | null
latestBeeRelease: LatestBeeRelease | null latestBeeRelease: LatestBeeRelease | null
isLoading: boolean isLoading: boolean
lastUpdate: number | null lastUpdate: number | null
@@ -91,6 +95,7 @@ const initialValues: ContextInterface = {
apiHealth: false, apiHealth: false,
nodeAddresses: null, nodeAddresses: null,
nodeInfo: null, nodeInfo: null,
nodeStatus: null,
topology: null, topology: null,
chequebookAddress: null, chequebookAddress: null,
stake: null, stake: null,
@@ -101,6 +106,7 @@ const initialValues: ContextInterface = {
settlements: null, settlements: null,
chainState: null, chainState: null,
walletBalance: null, walletBalance: null,
redistributionState: null,
latestBeeRelease: null, latestBeeRelease: null,
isLoading: true, isLoading: true,
lastUpdate: null, lastUpdate: null,
@@ -199,6 +205,7 @@ export function Provider({ children }: Props): ReactElement {
const [isWarmingUp, setIsWarmingUp] = useState<boolean>(true) const [isWarmingUp, setIsWarmingUp] = useState<boolean>(true)
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null) const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
const [nodeInfo, setNodeInfo] = useState<NodeInfo | null>(null) const [nodeInfo, setNodeInfo] = useState<NodeInfo | null>(null)
const [nodeStatus, setNodeStatus] = useState<DebugStatus | null>(null)
const [topology, setNodeTopology] = useState<Topology | null>(null) const [topology, setNodeTopology] = useState<Topology | null>(null)
const [chequebookAddress, setChequebookAddress] = useState<ChequebookAddressResponse | null>(null) const [chequebookAddress, setChequebookAddress] = useState<ChequebookAddressResponse | null>(null)
const [peers, setPeers] = useState<Peer[] | null>(null) const [peers, setPeers] = useState<Peer[] | null>(null)
@@ -209,6 +216,7 @@ export function Provider({ children }: Props): ReactElement {
const [settlements, setSettlements] = useState<AllSettlements | null>(null) const [settlements, setSettlements] = useState<AllSettlements | null>(null)
const [chainState, setChainState] = useState<ChainState | null>(null) const [chainState, setChainState] = useState<ChainState | null>(null)
const [walletBalance, setWalletBalance] = useState<WalletBalance | null>(null) const [walletBalance, setWalletBalance] = useState<WalletBalance | null>(null)
const [redistributionState, setRedistributionState] = useState<RedistributionState | null>(null)
const [startedAt, setStartedAt] = useState(() => Date.now()) const [startedAt, setStartedAt] = useState(() => Date.now())
const { latestBeeRelease } = useLatestBeeRelease() const { latestBeeRelease } = useLatestBeeRelease()
@@ -257,6 +265,7 @@ export function Provider({ children }: Props): ReactElement {
walletResult, walletResult,
chequebookBalanceResult, chequebookBalanceResult,
stakeResult, stakeResult,
redistributionStateResult,
peerBalancesResult, peerBalancesResult,
settlementsResult, settlementsResult,
] = await Promise.allSettled([ ] = await Promise.allSettled([
@@ -272,6 +281,7 @@ export function Provider({ children }: Props): ReactElement {
beeApi.getWalletBalance({ timeout: TIMEOUT }), beeApi.getWalletBalance({ timeout: TIMEOUT }),
beeApi.getChequebookBalance({ timeout: TIMEOUT }), beeApi.getChequebookBalance({ timeout: TIMEOUT }),
beeApi.getStake({ timeout: TIMEOUT }), beeApi.getStake({ timeout: TIMEOUT }),
beeApi.getRedistributionState({ timeout: TIMEOUT }),
beeApi.getAllBalances({ timeout: TIMEOUT }), beeApi.getAllBalances({ timeout: TIMEOUT }),
beeApi.getAllSettlements(), beeApi.getAllSettlements(),
]) ])
@@ -282,6 +292,7 @@ export function Provider({ children }: Props): ReactElement {
setApiHealth(Boolean(health)) setApiHealth(Boolean(health))
setIsWarmingUp(getFulfilledValue(statusResult)?.isWarmingUp ?? false) setIsWarmingUp(getFulfilledValue(statusResult)?.isWarmingUp ?? false)
setNodeStatus(getFulfilledValue(statusResult))
setNodeAddresses(getFulfilledValue(nodeAddressesResult)) setNodeAddresses(getFulfilledValue(nodeAddressesResult))
setNodeInfo(getFulfilledValue(nodeInfoResult)) setNodeInfo(getFulfilledValue(nodeInfoResult))
setNodeTopology(getFulfilledValue(topologyResult)) setNodeTopology(getFulfilledValue(topologyResult))
@@ -292,6 +303,7 @@ export function Provider({ children }: Props): ReactElement {
setWalletBalance(getFulfilledValue(walletResult)) setWalletBalance(getFulfilledValue(walletResult))
setChequebookBalance(getFulfilledValue(chequebookBalanceResult)) setChequebookBalance(getFulfilledValue(chequebookBalanceResult))
setStake(getFulfilledValue(stakeResult)) setStake(getFulfilledValue(stakeResult))
setRedistributionState(getFulfilledValue(redistributionStateResult))
setPeerBalances(getFulfilledValue(peerBalancesResult)?.balances ?? null) setPeerBalances(getFulfilledValue(peerBalancesResult)?.balances ?? null)
setSettlements(getFulfilledValue(settlementsResult)) setSettlements(getFulfilledValue(settlementsResult))
setError(null) setError(null)
@@ -332,6 +344,7 @@ export function Provider({ children }: Props): ReactElement {
setNodeAddresses(null) setNodeAddresses(null)
setNodeTopology(null) setNodeTopology(null)
setNodeInfo(null) setNodeInfo(null)
setNodeStatus(null)
setPeers(null) setPeers(null)
setChequebookAddress(null) setChequebookAddress(null)
setChequebookBalance(null) setChequebookBalance(null)
@@ -339,6 +352,7 @@ export function Provider({ children }: Props): ReactElement {
setPeerCheques(null) setPeerCheques(null)
setSettlements(null) setSettlements(null)
setChainState(null) setChainState(null)
setRedistributionState(null)
if (beeApi !== null) { if (beeApi !== null) {
refresh() refresh()
@@ -381,6 +395,7 @@ export function Provider({ children }: Props): ReactElement {
apiHealth, apiHealth,
nodeAddresses, nodeAddresses,
nodeInfo, nodeInfo,
nodeStatus,
topology, topology,
chequebookAddress, chequebookAddress,
peers, peers,
@@ -391,6 +406,7 @@ export function Provider({ children }: Props): ReactElement {
settlements, settlements,
chainState, chainState,
walletBalance, walletBalance,
redistributionState,
latestBeeRelease, latestBeeRelease,
isLoading, isLoading,
lastUpdate, lastUpdate,
@@ -405,6 +421,7 @@ export function Provider({ children }: Props): ReactElement {
apiHealth, apiHealth,
nodeAddresses, nodeAddresses,
nodeInfo, nodeInfo,
nodeStatus,
topology, topology,
chequebookAddress, chequebookAddress,
peers, peers,
@@ -415,6 +432,7 @@ export function Provider({ children }: Props): ReactElement {
settlements, settlements,
chainState, chainState,
walletBalance, walletBalance,
redistributionState,
latestBeeRelease, latestBeeRelease,
isLoading, isLoading,
lastUpdate, lastUpdate,
+1 -2
View File
@@ -1,11 +1,10 @@
import { createContext, ReactElement, ReactNode, useEffect, useState } from 'react' import { createContext, ReactElement, ReactNode, useEffect, useState } from 'react'
import { PREVIEW_DIMENSIONS } from '../constants' import { PREVIEW_DIMENSIONS } from '../constants'
import { FileOrigin } from '../pages/files/FileNavigation'
import { getMetadata } from '../utils/file' import { getMetadata } from '../utils/file'
import { resize } from '../utils/image' import { resize } from '../utils/image'
import { FileOrigin } from '@/pages/files/FileNavigation'
export type UploadOrigin = { origin: FileOrigin.Upload | FileOrigin.Feed; uuid?: string } export type UploadOrigin = { origin: FileOrigin.Upload | FileOrigin.Feed; uuid?: string }
export const defaultUploadOrigin: UploadOrigin = { origin: FileOrigin.Upload } export const defaultUploadOrigin: UploadOrigin = { origin: FileOrigin.Upload }
+3
View File
@@ -17,6 +17,7 @@ import { UploadLander } from './pages/files/UploadLander'
import GiftCards from './pages/giftCode' import GiftCards from './pages/giftCode'
import Info from './pages/info' import Info from './pages/info'
import PageNotFound from './pages/notFound/PageNotFound' import PageNotFound from './pages/notFound/PageNotFound'
import RedistributionPage from './pages/redistribution'
import LightModeRestart from './pages/restart/LightModeRestart' import LightModeRestart from './pages/restart/LightModeRestart'
import Settings from './pages/settings' import Settings from './pages/settings'
import { CreatePostageStampPage } from './pages/stamps/CreatePostageStampAdvancedPage' import { CreatePostageStampPage } from './pages/stamps/CreatePostageStampAdvancedPage'
@@ -38,6 +39,7 @@ export enum ROUTES {
UPLOAD_IN_PROGRESS = '/files/upload/workflow', UPLOAD_IN_PROGRESS = '/files/upload/workflow',
DOWNLOAD = '/files/download', DOWNLOAD = '/files/download',
HASH = '/files/hash/:hash', HASH = '/files/hash/:hash',
REDISTRIBUTION = '/redistribution',
SETTINGS = '/settings', SETTINGS = '/settings',
STATUS = '/status', STATUS = '/status',
TOP_UP = '/account/wallet/top-up', TOP_UP = '/account/wallet/top-up',
@@ -82,6 +84,7 @@ const BaseRouter = (): ReactElement => {
<Route path={ROUTES.SETTINGS} element={<Settings />} /> <Route path={ROUTES.SETTINGS} element={<Settings />} />
<Route path={ROUTES.STATUS} element={<Status />} /> <Route path={ROUTES.STATUS} element={<Status />} />
<Route path={ROUTES.INFO} element={<Info />} /> <Route path={ROUTES.INFO} element={<Info />} />
<Route path={ROUTES.REDISTRIBUTION} element={<RedistributionPage />} />
<Route path={ROUTES.TOP_UP} element={<TopUp />} /> <Route path={ROUTES.TOP_UP} element={<TopUp />} />
<Route path={ROUTES.TOP_UP_CRYPTO} element={<CryptoTopUpIndex />} /> <Route path={ROUTES.TOP_UP_CRYPTO} element={<CryptoTopUpIndex />} />
<Route path={ROUTES.TOP_UP_CRYPTO_SWAP} element={<Swap header="Top-up with cryptocurrencies" />} /> <Route path={ROUTES.TOP_UP_CRYPTO_SWAP} element={<Swap header="Top-up with cryptocurrencies" />} />
+25 -3
View File
@@ -1,5 +1,5 @@
import { EthAddress } from '@ethersphere/bee-js' import { EthAddress } from '@ethersphere/bee-js'
import { getAddress, JsonRpcProvider, Networkish } from 'ethers' import { getAddress, JsonRpcPayload, JsonRpcProvider, JsonRpcResult, Networkish } from 'ethers'
export const GNOIS_NETWORK_ID = 100 export const GNOIS_NETWORK_ID = 100
export const GnosisNetwork: Networkish = { chainId: GNOIS_NETWORK_ID, name: 'gnosis', ensAddress: undefined } export const GnosisNetwork: Networkish = { chainId: GNOIS_NETWORK_ID, name: 'gnosis', ensAddress: undefined }
@@ -39,6 +39,28 @@ export function ethAddressString(address: EthAddress | string): string {
return typeof address === 'string' ? getAddress(address) : getAddress(address.toHex()) return typeof address === 'string' ? getAddress(address) : getAddress(address.toHex())
} }
export function newGnosisProvider(url: string): JsonRpcProvider { /**
return new JsonRpcProvider(url, GnosisNetwork, { staticNetwork: true }) * Some RPC endpoints always return id:1 in their JSON-RPC responses regardless of the id in the request.
* Ethers v6 validates that response ids match request ids, so we patch them.
*/
class FixedIdJsonRpcProvider extends JsonRpcProvider {
async _send(payload: JsonRpcPayload | Array<JsonRpcPayload>): Promise<Array<JsonRpcResult>> {
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 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 })
} }
+116
View File
@@ -137,3 +137,119 @@ export function packageFile(file: FilePath, pathOverwrite?: string): FilePath {
bytes: file.bytes, 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<string, string> = {
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<string, string> | 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) =>
`<html><head><meta charset="utf-8"/><title>${title}</title></head><body style="margin:0;background:#000">
<video controls autoplay style="width:100%;height:100%" src="${u}"></video>
</body></html>`
const AUDIO_HTML = (u: string, title: string) =>
`<html><head><meta charset="utf-8"/><title>${title}</title></head><body>
<audio controls autoplay style="width:100%" src="${u}"></audio>
</body></html>`
const IMAGE_HTML = (u: string, title: string) =>
`<html><head><meta charset="utf-8"/><title>${title}</title></head><body style="margin:0;background:#111;display:grid;place-items:center;min-height:100vh">
<img style="max-width:100%;max-height:100vh" src="${u}" />
</body></html>`
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
},
},
]
+4 -3
View File
@@ -1,13 +1,14 @@
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 { randomBytes, Wallet } from 'ethers'
import { Identity, IdentityType } from '../providers/Feeds' import { Identity, IdentityType } from '../providers/Feeds'
import { LocalStorageKeys } from './localStorage' import { LocalStorageKeys } from './localStorage'
import { uuidV4, waitUntilStampUsable } from '.' import { waitUntilStampUsable } from './stamp'
import { uuidV4 } from '.'
export function generateWallet(): Wallet { export function generateWallet(): Wallet {
const privateKey = randomBytes(PrivateKey.LENGTH).toString() const privateKey = new Bytes(randomBytes(PrivateKey.LENGTH)).toString()
return new Wallet(privateKey) return new Wallet(privateKey)
} }
+1 -34
View File
@@ -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 { BigNumber } from 'bignumber.js'
import { BZZ_LINK_DOMAIN } from '../constants' 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)}` 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<PostageBatch> {
return waitForStamp(batchId, bee, options)
}
async function waitForStamp(batchId: BatchId | string, bee: Bee, options?: Options): Promise<PostageBatch> {
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')
}
+4 -4
View File
@@ -3,10 +3,10 @@ import { debounce } from '@mui/material'
import { Contract, JsonRpcProvider, TransactionReceipt, TransactionResponse, Wallet } from 'ethers' import { Contract, JsonRpcProvider, TransactionReceipt, TransactionResponse, Wallet } from 'ethers'
import { BZZ_TOKEN_ADDRESS, bzzABI } from './bzzAbi' import { BZZ_TOKEN_ADDRESS, bzzABI } from './bzzAbi'
import { ethAddressString, newGnosisProvider } from './chain' import { ethAddressString, newGnosisProvider, newGnosisProviderForValidation } from './chain'
async function getNetworkChainId(url: string): Promise<bigint> { async function getNetworkChainId(url: string): Promise<bigint> {
const provider = newGnosisProvider(url) const provider = newGnosisProviderForValidation(url)
const network = await provider.getNetwork() const network = await provider.getNetwork()
return network.chainId return network.chainId
@@ -84,7 +84,7 @@ export async function sendNativeTransaction(
const feedData = await signer.provider.getFeeData() const feedData = await signer.provider.getFeeData()
const gasPrice = externalGasPrice ?? DAI.fromWei(feedData.gasPrice?.toString() || '0') const gasPrice = externalGasPrice ?? DAI.fromWei(feedData.gasPrice?.toString() || '0')
const transaction = await signer.sendTransaction({ const transaction = await signer.sendTransaction({
to: to.toHex(), to: to.toChecksum(),
value: BigInt(value.toWeiString()), value: BigInt(value.toWeiString()),
gasPrice: BigInt(gasPrice.toWeiString()), gasPrice: BigInt(gasPrice.toWeiString()),
gasLimit: BigInt(21000), gasLimit: BigInt(21000),
@@ -117,7 +117,7 @@ export async function sendBzzTransaction(
const feeData = await signer.provider.getFeeData() const feeData = await signer.provider.getFeeData()
const gasPrice = feeData.gasPrice || BigInt(0) const gasPrice = feeData.gasPrice || BigInt(0)
const bzz = new Contract(BZZ_TOKEN_ADDRESS, bzzABI, signer) 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) const receipt = await transaction.wait(1)
if (!receipt) { if (!receipt) {
+63
View File
@@ -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<PostageBatch> {
return waitForStamp(batchId, bee, options)
}
async function waitForStamp(batchId: BatchId | string, bee: Bee, options?: Options): Promise<PostageBatch> {
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')
}
+52 -4
View File
@@ -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 { interface MockProvider {
getFeeData: jest.Mock getFeeData: jest.Mock
@@ -11,12 +11,14 @@ const mockWait = jest.fn()
const mockTransfer = jest.fn() const mockTransfer = jest.fn()
const mockGetFeeData = jest.fn() const mockGetFeeData = jest.fn()
const mockGetNetwork = jest.fn() const mockGetNetwork = jest.fn()
const mockSendTransaction = jest.fn()
const mockProvider: MockProvider = { const mockProvider: MockProvider = {
getFeeData: mockGetFeeData, getFeeData: mockGetFeeData,
getNetwork: mockGetNetwork, getNetwork: mockGetNetwork,
} }
const value = BZZ.fromDecimalString('1') const bzzValue = BZZ.fromDecimalString('1')
const daiValue = DAI.fromDecimalString('1')
const privateKey = 'FFFF000000000000000000000000000000000000000000000000000000000000' const privateKey = 'FFFF000000000000000000000000000000000000000000000000000000000000'
const jsonRpcProvider = 'http://mock-json-rpc-provider' const jsonRpcProvider = 'http://mock-json-rpc-provider'
@@ -39,6 +41,7 @@ jest.mock('ethers', () => {
class Wallet { class Wallet {
provider: MockProvider provider: MockProvider
sendTransaction = mockSendTransaction
constructor(_privateKey: string, provider: MockProvider) { constructor(_privateKey: string, provider: MockProvider) {
this.provider = provider this.provider = provider
@@ -64,8 +67,53 @@ describe('sendBzzTransaction', () => {
}) })
it.each(addresses)('sendBzzTransaction to address: %s', async (address: string) => { 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 const to = mockTransfer.mock.calls[0][0] as string
expect(to.startsWith('0x')).toBe(true) 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()))
})
}) })
+1 -1
View File
@@ -59,7 +59,7 @@ export default defineConfig(({ mode }) => {
plugins: [ plugins: [
react(), react(),
nodePolyfills({ nodePolyfills({
include: ['util', 'buffer'], include: ['util', 'buffer', 'stream'],
globals: { globals: {
Buffer: true, Buffer: true,
global: true, global: true,