diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1ee0d4d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +npm-debug.log +build +.dockerignore +**/.git +**/.DS_Store +**/node_modules diff --git a/.github/logo.svg b/.github/logo.svg new file mode 100644 index 0000000..b4da076 --- /dev/null +++ b/.github/logo.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..9dc1868 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,67 @@ +name: Build + +on: + pull_request: + branches: + - master + types: [opened, synchronize] + paths-ignore: + - '**/*.md' + push: + # Build for the master branch. + branches: + - master + release: + types: + - published + workflow_dispatch: + inputs: + ref: + description: 'Ref to build [default: latest master; examples: v0.4.0, 9595da7d83efc330ca0bc94bef482e4edfbcf8fd]' + required: false + default: '' + deploy: + description: 'Deploy to production [default: false; examples: true, false]' + required: false + default: 'false' + +jobs: + build_release: + name: Build and deploy + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.ref }} + # Allows to fetch all history for all branches and tags. Need this for proper versioning. + fetch-depth: 0 + + - name: Build + run: make release + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: release + path: ./archive.fs.neo.org-*.tar.gz + if-no-files-found: error + + - name: Attach binary to the release as an asset + if: ${{ github.event_name == 'release' }} + run: gh release upload ${{ github.event.release.tag_name }} ./archive.fs.neo.org-*.tar.gz + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish to NeoFS + if: ${{ github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy == 'true') }} + uses: nspcc-dev/gh-push-to-neofs@master + with: + NEOFS_WALLET: ${{ secrets.NEOFS_WALLET }} + NEOFS_WALLET_PASSWORD: ${{ secrets.NEOFS_WALLET_PASSWORD }} + NEOFS_NETWORK_DOMAIN: ${{ vars.NEOFS_NETWORK_DOMAIN }} + NEOFS_HTTP_GATE: ${{ vars.NEOFS_HTTP_GATE }} + STORE_OBJECTS_CID: ${{ vars.STORE_OBJECTS_CID }} + PATH_TO_FILES_DIR: archive.fs.neo.org + STRIP_PREFIX: true + REPLACE_CONTAINER_CONTENTS: true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fe27d5d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +Changelog for NeoFS Archive + +## [Unreleased] + + +## [0.1.0] - 2025-01-15 + +First public review release. + + +[0.1.0]: https://github.com/nspcc-dev/archive-fs-neo-org/tree/v0.1.0 +[Unreleased]: https://github.com/nspcc-dev/archive-fs-neo-org/compare/v0.1.0...master diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0ea7757 --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +#!/usr/bin/make -f + +SHELL = bash + +VERSION ?= "$(shell git describe --tags --match "v*" --abbrev=8 2>/dev/null | sed -r 's,^v([0-9]+\.[0-9]+)\.([0-9]+)(-.*)?$$,\1 \2 \3,' | while read mm patch suffix; do if [ -z "$$suffix" ]; then echo $$mm.$$patch; else patch=`expr $$patch + 1`; echo $$mm.$${patch}-pre$$suffix; fi; done)" +SITE_DIR ?= archive.fs.neo.org +RELEASE_DIR ?= $(SITE_DIR)-$(VERSION) +RELEASE_PATH ?= $(SITE_DIR)-$(VERSION).tar.gz +CURRENT_UID ?= $(shell id -u $$USER) + +PORT = 3000 + +$(SITE_DIR): + docker run \ + -v $$(pwd)/src:/usr/src/app/src \ + -v $$(pwd)/public:/usr/src/app/public \ + -v $$(pwd)/package.json:/usr/src/app/package.json \ + -v $$(pwd)/$(SITE_DIR):/usr/src/app/$(SITE_DIR) \ + -e CURRENT_UID=$(CURRENT_UID) \ + -w /usr/src/app node:14-alpine \ + sh -c 'npm install && REACT_APP_VERSION=$(VERSION) npm run build && chown -R $$CURRENT_UID: $(SITE_DIR)' + +start: + docker run \ + -p $(PORT):3000 \ + -v `pwd`:/usr/src/app \ + -w /usr/src/app node:14-alpine \ + sh -c 'npm install --silent && npm run build && npm install -g serve && serve -s $(SITE_DIR) -p 3000' + +release: $(SITE_DIR) + cp $(SITE_DIR)/index.html $(SITE_DIR)/agreement + @ln -sf $(SITE_DIR) $(RELEASE_DIR) + @tar cfvhz $(RELEASE_PATH) $(RELEASE_DIR) + +clean: + @echo "Cleaning up ..." + @rm -rf $(SITE_DIR) $(RELEASE_DIR) $(RELEASE_PATH) + +release_name: + @echo $(RELEASE_PATH) + +version: + @echo $(VERSION) diff --git a/README.md b/README.md new file mode 100644 index 0000000..f3cf412 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +

+NeoFS +

+

+ NeoFS is a decentralized distributed object storage integrated with the Neo Blockchain. +

+ +--- +![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/nspcc-dev/archive-fs-neo-org?sort=semver) +![License](https://img.shields.io/github/license/nspcc-dev/archive-fs-neo-org.svg?style=popout) + +# Overview + +Archive.NeoFS – Download Blockchain Data Snapshot. Download an offline package of the blockchain data up to a certain block height. This web application is built on the React framework. + +# Requirements + +- docker +- make +- node (`14+`) + +# Make instructions + +* Compile the build using `make` (will be generated in `archive-fs-neo-org` dir) +* Start app using `make start PORT=3000` (PORT=3000 by default) +* Clean up cache directories using `make clean` +* Get release directory with tar.gz using `make release` + +# License + +- [GNU General Public License v3.0](LICENSE) diff --git a/package.json b/package.json new file mode 100644 index 0000000..216ee40 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "archive-fs-neo-org", + "version": "0.0.1", + "private": true, + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.4.0", + "@fortawesome/free-brands-svg-icons": "^6.4.0", + "@fortawesome/free-regular-svg-icons": "^6.4.0", + "@fortawesome/free-solid-svg-icons": "^6.4.0", + "@fortawesome/react-fontawesome": "^0.2.0", + "@types/react": "^18.2.41", + "@types/react-dom": "^18.2.17", + "bulma": "^0.9.4", + "react": "^17.0.2", + "react-bulma-components": "^4.1.0", + "react-dom": "^17.0.2", + "react-router-dom": "^6.10.0" + }, + "scripts": { + "start": "REACT_APP_VERSION=$(make version) GENERATE_SOURCEMAP=false react-scripts start", + "build": "GENERATE_SOURCEMAP=false BUILD_PATH='./archive.fs.neo.org' react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "devDependencies": { + "dotenv": "^16.0.3", + "react-scripts": "^5.0.1", + "typescript": "^4.9.5" + } +} diff --git a/public/img/close.svg b/public/img/close.svg new file mode 100644 index 0000000..73396e3 --- /dev/null +++ b/public/img/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/cover.png b/public/img/cover.png new file mode 100644 index 0000000..44df980 Binary files /dev/null and b/public/img/cover.png differ diff --git a/public/img/favicon.ico b/public/img/favicon.ico new file mode 100644 index 0000000..5a1c553 Binary files /dev/null and b/public/img/favicon.ico differ diff --git a/public/img/logo.svg b/public/img/logo.svg new file mode 100644 index 0000000..2081f97 --- /dev/null +++ b/public/img/logo.svg @@ -0,0 +1,103 @@ + + + + + + image/svg+xml + + NeoFS + + + + + + + + NeoFS + + + + + + + + + + + + diff --git a/public/img/socials/github.svg b/public/img/socials/github.svg new file mode 100644 index 0000000..aa05db9 --- /dev/null +++ b/public/img/socials/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/socials/medium.svg b/public/img/socials/medium.svg new file mode 100644 index 0000000..08a9433 --- /dev/null +++ b/public/img/socials/medium.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/public/img/socials/neo.svg b/public/img/socials/neo.svg new file mode 100644 index 0000000..75e2d8b --- /dev/null +++ b/public/img/socials/neo.svg @@ -0,0 +1,64 @@ + + + + + + image/svg+xml + + Asset 9 + + + + + + + + Asset 9 + + + diff --git a/public/img/socials/neo_spcc.svg b/public/img/socials/neo_spcc.svg new file mode 100644 index 0000000..d0b04c6 --- /dev/null +++ b/public/img/socials/neo_spcc.svg @@ -0,0 +1,108 @@ + + + + + + image/svg+xml + + Asset 9 + + + + + + + + Asset 9 + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/socials/twitter.svg b/public/img/socials/twitter.svg new file mode 100644 index 0000000..1970575 --- /dev/null +++ b/public/img/socials/twitter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/socials/youtube.svg b/public/img/socials/youtube.svg new file mode 100644 index 0000000..6c30aa1 --- /dev/null +++ b/public/img/socials/youtube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..10dddf3 --- /dev/null +++ b/public/index.html @@ -0,0 +1,28 @@ + + + + + Archive.NeoFS + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..4f2ada7 --- /dev/null +++ b/src/App.css @@ -0,0 +1,393 @@ +body { + margin: 0; + padding: 0; + color: #111827; + background: #fff; + font-family: 'Poppins', sans-serif; + min-width: 300px; + -webkit-tap-highlight-color: rgba(255, 255, 255, 0); +} + +a { + text-decoration: none; +} + +:focus { + outline-color: #02af92; +} + +.input:active, +.input:focus, +.is-active.input, +.is-active.textarea, +.is-focused.input, +.is-focused.textarea, +.select select.is-active, +.select select.is-focused, +.select select:active, +.select select:focus, +.textarea:active, +.textarea:focus { + border-color: #02af92; +} + +.input[disabled] { + border-color: #dbdbdb; +} + +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.select select[disabled] { + border-color: #dbdbdb !important; +} + +.select:not(.is-multiple):not(.is-loading)::after { + border-color: #02af92; +} + +.select_block { + display: flex; + align-items: center; + flex-direction: column; +} + +.select_block select { + min-width: 300px; +} + +.inputs_block { + display: flex; + justify-content: center; +} + +.inputs_block input { + width: 145px; + margin: 0 5px; +} + +.inputs_block .control { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.inputs_block .control .tag { + margin: .3rem 0 0 5px; + background-color: #ffffff; + cursor: pointer; +} + +progress { + border-radius: 6px !important; + text-align: center; + margin: 10px auto !important; + max-width: 90%; + height: 10px !important; +} + +progress::-webkit-progress-value { + background: #02af92 !important; +} + +.navbar-item, +.navbar-link { + color: #ffffff80 !important; + background: transparent !important; +} + +.notification a:not(.button):not(.dropdown-item) { + text-decoration: none; +} + +.notification { + padding: 1.25rem 1.5rem 1.25rem 1.5rem; +} + +a.navbar-item:hover, +div.navbar-item:hover { + cursor: pointer; + color: #fff !important; +} + +.navbar, +.navbar-menu { + background: #29363b; +} + +.navbar-burger { + color: #ffffff; +} + +.tooltip { + position: absolute; + min-width: 70px; + background: #29363b; + text-align: center; + padding: 4px 8px; + font-size: 12px; + border-radius: 4px; + color: #fff; + top: -80%; +} + +.tooltip:after { + position: absolute; + border: solid transparent; + content: ""; + height: 0; + width: 0; + top: 100%; + right: 50%; + border-width: 6px; + margin: -2px -6px; + border-top-color: #29363b; +} + +.socials { + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 20px; +} + +.socials a { + line-height: 0; + margin: 0 10px; +} + +.social_pipe { + border-right: 2px solid rgb(0, 0, 0); + padding-right: 10px; + display: flex; +} + +.button { + outline: none; + box-shadow: unset !important; +} + +.button.is-primary, +.notification.is-primary { + color: #fff; + background: #02af92; + border-color: #02af92; +} + +.notification.is-primary { + padding: 0.5rem 1rem; + margin-bottom: 0.5rem; +} + +.notification>.delete { + right: 1rem; + top: 0.65rem; +} + +.file.is-boxed .file-cta { + border-style: dashed; + border-width: 2px; +} + +.file-cta, +.file-name { + white-space: normal; + text-align: center; +} + +.file.is-boxed .file-icon { + height: 3.5em; +} + +.label { + font-weight: 400; +} + +.button.is-focused, +.button:focus { + border-color: inherit; +} + +.footer .subtitle { + line-height: 1.5; +} + +code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + white-space: break-spaces; + background-color: #eff1f2; + color: inherit; + border-radius: 6px; +} + +.content pre { + color: inherit; + border-radius: 4px; +} + +/* modal */ +.modal { + position: fixed; + z-index: 102; + display: flex; + justify-content: center; + align-items: center; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.4); +} + +.modal_close_panel { + position: fixed; + z-index: 102; + display: flex; + justify-content: center; + align-items: center; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; +} + +.modal_content { + position: absolute; + background: #fff; + border-radius: 4px; + z-index: 103; + padding: 1.25rem 1.5rem 1.25rem 1.5rem; + min-width: 300px; + max-width: 350px; + margin: 10px; +} + +.modal_scroll { + overflow-y: auto; + width: 100%; + display: flex; + justify-content: center; + align-items: flex-start; +} + +.modal_scroll .modal_content { + position: relative; +} + +.modal_close { + padding: 5px; + position: absolute; + top: 0; + right: 0; +} + +.modal_close img { + cursor: pointer; +} + +.modal_loader { + display: flex; + margin: 5px auto 15px; + -webkit-animation: pulse 1.5s infinite linear; + animation: pulse 1.5s infinite linear; +} + +@media (prefers-color-scheme: dark) { + html { + background: #2d333b; + } + + body { + color: #adbac7; + background: #22272d; + } + + .navbar-menu, + .footer, + .modal_content { + background: #2d333b; + } + + .subtitle, + .navbar-item, + .navbar-link, + .label { + color: #adbac7 !important; + } + + .notification code, + .notification pre { + background: #343942; + } + + .notification.is-primary .subtitle { + color: #fff !important; + } + + .navbar, + .file-cta:hover { + background: #2d333b !important; + } + + .notification { + background: #22272d; + border: 2px solid #343942; + } + + .file-cta { + background: #22272d; + border-color: #343942; + color: #adbac7 !important; + } + + .input, + .select select, + .textarea { + color: #adbac7; + background-color: #22272d; + border-color: #343942; + outline: none !important; + } + + .select:not(.is-multiple):not(.is-loading)::after { + border-color: #adbac7; + } + + .socials a { + filter: invert(1); + } + + .social_pipe { + border-color: #fff; + } +} + +@media (min-width: 1025px) { + .navbar-menu { + margin-right: 6rem; + } + + .navbar-brand { + margin-left: 6rem; + } +} + +@media (max-width: 500px) { + .title { + font-size: 20px; + } + + .section { + padding: 1.5rem 1rem; + } + + .notification { + padding: 1rem; + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..9286bc5 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,220 @@ +import React, { useState } from 'react'; +import { Route, Routes } from "react-router-dom"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { + Navbar, + Heading, + Footer, + Progress, +} from 'react-bulma-components'; +import Home from './Home.tsx'; +import NotFound from './NotFound.tsx'; +import 'bulma/css/bulma.min.css'; +import './App.css'; + +import { + faSpinner, + faDownload, +} from '@fortawesome/free-solid-svg-icons'; + +library.add( + faDownload, + faSpinner, +); + +interface NetItem { + title: string + containerId: string + rpc: string + maxBlock: number +} + +interface Modal { + current: string | null + params: any +} + +export const App = () => { + const [nets, setNets] = useState([{ + title: 'Mainnet', + containerId: '3RCdP3ZubyKyo8qFeo7EJPryidTZaGCMdUjqFJaaEKBV', + rpc: 'https://rpc10.n3.nspcc.ru:10331', + maxBlock: 0, + }, { + title: 'Testnet', + containerId: 'A8nGtDemWrm2SjfcGAG6wvrxmXwqc5fwr8ezNDm6FraT', + rpc: 'https://rpc.t5.n3.nspcc.ru:20331', + maxBlock: 0, + }, { + title: 'NeoFS Mainnet', + containerId: 'BP71MqY7nJhpuHfdQU3infRSjMgVmSFFt9GfG2GGMZJj', + rpc: 'https://rpc.morph.fs.neo.org', + maxBlock: 0, + }, { + title: 'NeoFS Testnet', + containerId: '98xz5YeanzxRCpH6EfUhECVm2MynGYchDN4naJViHT9M', + rpc: 'https://rpc1.morph.t5.fs.neo.org', + maxBlock: 0, + }]); + const [currentDownloadedBlock, setCurrentDownloadedBlock] = useState(0); + const [isLoading, setLoading] = useState(false); + const [modal, setModal] = useState({ + current: null, + params: '', + }); + + const onModal = (current: string | null = null, params: any = null) => { + setModal({ current, params }); + }; + + const roundNumber = (num: number): number => { + const rounded = num.toFixed(2); + return parseFloat(rounded) % 1 === 0 ? parseInt(rounded) : parseFloat(rounded); + }; + + return ( + <> + {(modal.current === 'success' || modal.current === 'failed') && ( +
+
onModal()} + /> +
+
onModal()} + > + close +
+ {modal.current === 'success' ? 'Success' : 'Failed'} +

{modal.params}

+
+
+ )} + {modal.current === 'loading' && ( +
+
{} : () => onModal()} + /> +
+ {`Snapshot`} + {`${modal.params.spanStart} - ${modal.params.spanEnd} (${nets[modal.params.network].title})`} + {currentDownloadedBlock / (modal.params.spanEnd - modal.params.spanStart + 1) === 1 ? 'Success!' : 'Writing'} + + {`${currentDownloadedBlock} / ${modal.params.spanEnd - modal.params.spanStart + 1} (${roundNumber((currentDownloadedBlock / (modal.params.spanEnd - modal.params.spanStart + 1)) * 100)}%)`} +
+
+ )} + + + + logo + + + +
+ + } + /> + } + /> + +
+ + + ); +} diff --git a/src/Home.tsx b/src/Home.tsx new file mode 100644 index 0000000..bb6772d --- /dev/null +++ b/src/Home.tsx @@ -0,0 +1,235 @@ +import React, { useState, useEffect } from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + Content, + Container, + Form, + Section, + Heading, + Tile, + Tag, + Notification, + Button, +} from 'react-bulma-components'; +import api from './api.ts'; + +interface NetItem { + title: string + containerId: string +} + +interface FormData { + spanStart: number | '' + spanEnd: number | '' + network: number +} + +const Home = ({ + onModal, + nets, + setNets, + setCurrentDownloadedBlock, + isLoading, + setLoading, +}) => { + const [formData, setFormData] = useState({ + spanStart: '', + spanEnd: '', + network: 0, + }); + + useEffect(() => { + if (nets[formData.network].maxBlock === 0) { + api('POST', nets[formData.network].rpc, { + "jsonrpc": "2.0", + "id": 1, + "method": "getblockcount", + "params": [] + }).then((res: any) => { + const netsTemp = [...nets]; + netsTemp[formData.network].maxBlock = Math.floor(res.result / 128000) * 128000; + setNets(netsTemp); + + if (formData.spanEnd > netsTemp[formData.network].maxBlock) return setFormData({ ...formData, spanEnd: nets[formData.network].maxBlock }); + }).catch(() => { + onModal('failed', 'Object not found: missing or expired'); + }); + } else { + if (formData.spanEnd > nets[formData.network].maxBlock) return setFormData({ ...formData, spanEnd: nets[formData.network].maxBlock }); + } + },[formData.network]); // eslint-disable-line react-hooks/exhaustive-deps + + const fetchBlocksInRange = async () => { + if (formData.spanStart === '' || formData.spanEnd === '' || formData.spanEnd < 0) return onModal('failed', 'Insert correct data'); + if (formData.spanStart < 0 || formData.spanEnd < 0 || formData.spanStart > nets[formData.network].maxBlock || formData.spanEnd > nets[formData.network].maxBlock) return onModal('failed', 'Insert correct borders'); + + onModal('loading', formData); + setLoading(true); + const currentNet: NetItem = nets[formData.network]; + + try { + const fileHandle = await window.showSaveFilePicker({ + suggestedName: `snapshot_${nets[formData.network].title}_${formData.spanStart}-${formData.spanEnd}.acc`, + types: [ + { + description: 'ACC Files', + accept: { + 'application/octet-stream': ['.acc'], + }, + }, + ], + }); + + const writableStream = await fileHandle.createWritable(); + + const blockCount = new Int32Array([formData.spanEnd - formData.spanStart + 1]); + await writableStream.write(blockCount.buffer); + + for (let block = formData.spanStart; block <= formData.spanEnd; block++) { + const objectData: Uint8Array | string = await fetchBlock(currentNet, block); + if (typeof objectData === 'string') { + onModal('failed', objectData); + await writableStream.close(); + return + } + + const blockSize = new Uint32Array([objectData.byteLength]); + await writableStream.write(blockSize.buffer); + await writableStream.write(new Uint8Array(objectData)); + setCurrentDownloadedBlock(block - formData.spanStart + 1); + } + + await writableStream.close(); + } catch (error: any) { + onModal('failed', error.message || 'Error occurred during block fetching.'); + } finally { + setLoading(false); + } + }; + + const fetchBlock = async (currentNet: NetItem, blockNumber: number): Promise => { + try { + const searchResponse: any = await api('POST', `/objects/${currentNet.containerId}/search?walletConnect=false&offset=0&limit=1`, { + filters: [{ + "key": "Block", + "match": "MatchStringEqual", + "value": blockNumber.toString(), + }], + }); + + const objectId = searchResponse.objects[0]?.address.objectId; + if (!objectId) { + return `Error occurred during block fetching #${blockNumber}`; + } + + const blockResponse = await api('GET', `/objects/${currentNet.containerId}/by_id/${objectId}?walletConnect=false`); + return blockResponse as Uint8Array; + } catch (err: any) { + return `Error occurred during block fetching #${blockNumber}: ${err.message}`; + } + }; + + return ( + +
+ + + + Archive.NeoFS – Download Blockchain Data Snapshot + +

Easily download an offline package of blockchain data up to a specific block height.

+

Manual steps:

+
    +
  • Choose start and end snapshot option for the data range;
  • +
  • Select the desired network;
  • +
  • Click the Download button;
  • +
  • Wait for the full blockchain data to prepared;
  • +
  • Download the .acc file to your device. 🚀
  • +
+
+
+
+
+ + + + Prepare snapshot + + + { + if (e.target.value === '' || /^[0-9]*[.]?[0-9]*$/.test(e.target.value)) { + setFormData({ ...formData, spanStart: e.target.value >= 0 ? e.target.value : '' }); + } + }} + disabled={isLoading} + /> + setFormData({ ...formData, spanStart: 0 })} + >min: 0 + + + { + if (e.target.value === '' || /^[0-9]*[.]?[0-9]*$/.test(e.target.value)) { + setFormData({ ...formData, spanEnd: e.target.value >= 0 ? e.target.value : '' }); + } + }} + disabled={isLoading} + /> + setFormData({ ...formData, spanEnd: nets[formData.network].maxBlock })} + >{`max: ${nets[formData.network].maxBlock ? nets[formData.network].maxBlock : '-'}`} + + + + + setFormData({ ...formData, network: Number(e.target.value) })} + value={formData.network} + disabled={isLoading} + > + + + + + + + + + + + + + +
+
+ ); +} + +export default Home; diff --git a/src/NotFound.tsx b/src/NotFound.tsx new file mode 100644 index 0000000..b1f393d --- /dev/null +++ b/src/NotFound.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { + Container, + Section, + Heading, + Tile, + Notification, + Button, +} from 'react-bulma-components'; + +const NotFound = () => { + return ( + +
+ + + + 404 Not Found + Page not found + + + + + + +
+
+ ); +}; + +export default NotFound; diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..24577ef --- /dev/null +++ b/src/api.ts @@ -0,0 +1,50 @@ +const server = 'https://rest.fs.neo.org/v1'; + +type Methods = "GET" | "POST"; + +async function serverRequest(method: Methods, url: string, params: object, headers: any) { + const json: any = { + method, + headers, + } + + if (json['headers']['Content-Type']) { + json['body'] = params; + } else if (Object.keys(params).length > 0) { + json['body'] = JSON.stringify(params); + json['headers']['Content-Type'] = 'application/json'; + } + + let activeUrl: string = url; + if (url.indexOf('https') === -1) { + activeUrl = `${server}${url}`; + } + + return fetch(activeUrl, json).catch((error: any) => error); +} + +export default function api(method: Methods, url: string, params: object = {}, headers: object = {}) { + return new Promise((resolve, reject) => { + serverRequest(method, url, params, headers).then(async (response: any) => { + if (response && response.status === 204) { + resolve({ status: 'success' }); + } else { + let res: any = response; + if (response?.status === 200 && url.indexOf('by_id') !== -1) { + res = await response.arrayBuffer(); + resolve(res); + } else if (response?.status === 200) { + res = await response.json(); + resolve(res); + } else { + try { + res = await response.json(); + reject(res); + } catch (err) { + reject(response); + } + } + } + }); + }); +} diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..6302304 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { App } from './App.tsx'; +import { BrowserRouter } from "react-router-dom"; + +ReactDOM.render( + + + + + , + document.getElementById('root') +);