diff --git a/package-lock.json b/package-lock.json index 6825d07..68c548f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2686,62 +2686,6 @@ "@ethersproject/logger": "^5.6.0" } }, - "node_modules/@ethersproject/providers": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.6.4.tgz", - "integrity": "sha512-WAdknnaZ52hpHV3qPiJmKx401BLpup47h36Axxgre9zT+doa/4GC/Ne48ICPxTm0BqndpToHjpLP1ZnaxyE+vw==", - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "dependencies": { - "@ethersproject/abstract-provider": "^5.6.0", - "@ethersproject/abstract-signer": "^5.6.0", - "@ethersproject/address": "^5.6.0", - "@ethersproject/basex": "^5.6.0", - "@ethersproject/bignumber": "^5.6.0", - "@ethersproject/bytes": "^5.6.0", - "@ethersproject/constants": "^5.6.0", - "@ethersproject/hash": "^5.6.0", - "@ethersproject/logger": "^5.6.0", - "@ethersproject/networks": "^5.6.0", - "@ethersproject/properties": "^5.6.0", - "@ethersproject/random": "^5.6.0", - "@ethersproject/rlp": "^5.6.0", - "@ethersproject/sha2": "^5.6.0", - "@ethersproject/strings": "^5.6.0", - "@ethersproject/transactions": "^5.6.0", - "@ethersproject/web": "^5.6.0", - "bech32": "1.1.4", - "ws": "7.4.6" - } - }, - "node_modules/@ethersproject/providers/node_modules/ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/@ethersproject/random": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.6.0.tgz", @@ -11038,6 +10982,62 @@ "@ethersproject/wordlists": "5.6.0" } }, + "node_modules/ethers/node_modules/@ethersproject/providers": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.6.4.tgz", + "integrity": "sha512-WAdknnaZ52hpHV3qPiJmKx401BLpup47h36Axxgre9zT+doa/4GC/Ne48ICPxTm0BqndpToHjpLP1ZnaxyE+vw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-provider": "^5.6.0", + "@ethersproject/abstract-signer": "^5.6.0", + "@ethersproject/address": "^5.6.0", + "@ethersproject/basex": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/constants": "^5.6.0", + "@ethersproject/hash": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/networks": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/random": "^5.6.0", + "@ethersproject/rlp": "^5.6.0", + "@ethersproject/sha2": "^5.6.0", + "@ethersproject/strings": "^5.6.0", + "@ethersproject/transactions": "^5.6.0", + "@ethersproject/web": "^5.6.0", + "bech32": "1.1.4", + "ws": "7.4.6" + } + }, + "node_modules/ethers/node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -31142,40 +31142,6 @@ "@ethersproject/logger": "^5.6.0" } }, - "@ethersproject/providers": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.6.4.tgz", - "integrity": "sha512-WAdknnaZ52hpHV3qPiJmKx401BLpup47h36Axxgre9zT+doa/4GC/Ne48ICPxTm0BqndpToHjpLP1ZnaxyE+vw==", - "requires": { - "@ethersproject/abstract-provider": "^5.6.0", - "@ethersproject/abstract-signer": "^5.6.0", - "@ethersproject/address": "^5.6.0", - "@ethersproject/basex": "^5.6.0", - "@ethersproject/bignumber": "^5.6.0", - "@ethersproject/bytes": "^5.6.0", - "@ethersproject/constants": "^5.6.0", - "@ethersproject/hash": "^5.6.0", - "@ethersproject/logger": "^5.6.0", - "@ethersproject/networks": "^5.6.0", - "@ethersproject/properties": "^5.6.0", - "@ethersproject/random": "^5.6.0", - "@ethersproject/rlp": "^5.6.0", - "@ethersproject/sha2": "^5.6.0", - "@ethersproject/strings": "^5.6.0", - "@ethersproject/transactions": "^5.6.0", - "@ethersproject/web": "^5.6.0", - "bech32": "1.1.4", - "ws": "7.4.6" - }, - "dependencies": { - "ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", - "requires": {} - } - } - }, "@ethersproject/random": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.6.0.tgz", @@ -37681,6 +37647,40 @@ "@ethersproject/wallet": "5.6.0", "@ethersproject/web": "5.6.0", "@ethersproject/wordlists": "5.6.0" + }, + "dependencies": { + "@ethersproject/providers": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.6.4.tgz", + "integrity": "sha512-WAdknnaZ52hpHV3qPiJmKx401BLpup47h36Axxgre9zT+doa/4GC/Ne48ICPxTm0BqndpToHjpLP1ZnaxyE+vw==", + "requires": { + "@ethersproject/abstract-provider": "^5.6.0", + "@ethersproject/abstract-signer": "^5.6.0", + "@ethersproject/address": "^5.6.0", + "@ethersproject/basex": "^5.6.0", + "@ethersproject/bignumber": "^5.6.0", + "@ethersproject/bytes": "^5.6.0", + "@ethersproject/constants": "^5.6.0", + "@ethersproject/hash": "^5.6.0", + "@ethersproject/logger": "^5.6.0", + "@ethersproject/networks": "^5.6.0", + "@ethersproject/properties": "^5.6.0", + "@ethersproject/random": "^5.6.0", + "@ethersproject/rlp": "^5.6.0", + "@ethersproject/sha2": "^5.6.0", + "@ethersproject/strings": "^5.6.0", + "@ethersproject/transactions": "^5.6.0", + "@ethersproject/web": "^5.6.0", + "bech32": "1.1.4", + "ws": "7.4.6" + } + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "requires": {} + } } }, "event-target-shim": { diff --git a/src/App.tsx b/src/App.tsx index e1a2acf..430dc45 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,7 @@ import { Provider as FileProvider } from './providers/File' import { Provider as PlatformProvider } from './providers/Platform' import { Provider as SettingsProvider } from './providers/Settings' import { Provider as StampsProvider } from './providers/Stamps' +import { Provider as TopUpProvider } from './providers/TopUp' import BaseRouter from './routes' import { theme } from './theme' @@ -29,16 +30,18 @@ const App = ({ beeApiUrl, beeDebugApiUrl, lockedApiSettings }: Props): ReactElem - - - <> - - - - - - - + + + + <> + + + + + + + + diff --git a/src/components/AlertVersion.tsx b/src/components/AlertVersion.tsx deleted file mode 100644 index 44fea3a..0000000 --- a/src/components/AlertVersion.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { ReactElement, useState, useContext } from 'react' -import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' -import { Alert, AlertTitle } from '@material-ui/lab' -import Collapse from '@material-ui/core/Collapse' -import IconButton from '@material-ui/core/IconButton' -import CloseIcon from '@material-ui/icons/Close' -import { Context } from '../providers/Bee' -import { SUPPORTED_BEE_VERSION_EXACT } from '@ethersphere/bee-js' - -const useStyles = makeStyles((theme: Theme) => - createStyles({ - root: { - width: '100%', - marginBottom: theme.spacing(2), - }, - }), -) - -export default function VersionAlert(): ReactElement | null { - const classes = useStyles() - const { isLoading, latestUserVersionExact } = useContext(Context) - const [open, setOpen] = useState(true) - - const isExactlySupportedBeeVersion = SUPPORTED_BEE_VERSION_EXACT === latestUserVersionExact - - if (isLoading || !latestUserVersionExact) return null - - return ( - -
- { - setOpen(false) - }} - > - - - } - > - Warning - Your Bee node version ({latestUserVersionExact}) does not exactly match the Bee version we tested - the Bee Dashboard against ({SUPPORTED_BEE_VERSION_EXACT}). Please note that some functionality - may not work properly. - -
-
- ) -} diff --git a/src/components/CashoutModal.tsx b/src/components/CashoutModal.tsx index 36a7b91..4e66c55 100644 --- a/src/components/CashoutModal.tsx +++ b/src/components/CashoutModal.tsx @@ -6,7 +6,7 @@ import DialogContent from '@material-ui/core/DialogContent' import DialogContentText from '@material-ui/core/DialogContentText' import DialogTitle from '@material-ui/core/DialogTitle' import { useSnackbar } from 'notistack' -import { ReactElement, useState, useContext } from 'react' +import { ReactElement, useContext, useState } from 'react' import { Zap } from 'react-feather' import { Context as SettingsContext } from '../providers/Settings' import EthereumAddress from './EthereumAddress' @@ -61,7 +61,7 @@ export default function CheckoutModal({ peerId, uncashedAmount }: Props): ReactE return (
Cashout Cheque diff --git a/src/components/ExpandableListItemKey.tsx b/src/components/ExpandableListItemKey.tsx index c874a41..2d4587f 100644 --- a/src/components/ExpandableListItemKey.tsx +++ b/src/components/ExpandableListItemKey.tsx @@ -38,6 +38,7 @@ const useStyles = makeStyles((theme: Theme) => interface Props { label: string value: string + expanded?: boolean } const lengthWithoutPrefix = (s: string) => s.replace(/^0x/i, '').length @@ -54,9 +55,9 @@ const split = (s: string): string[] => { return s.match(/(0x|.{1,8})/gi) || [] } -export default function ExpandableListItemKey({ label, value }: Props): ReactElement | null { +export default function ExpandableListItemKey({ label, value, expanded }: Props): ReactElement | null { const classes = useStyles() - const [open, setOpen] = useState(false) + const [open, setOpen] = useState(expanded || false) const [copied, setCopied] = useState(false) const toggleOpen = () => setOpen(!open) diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index dc2f4c2..0348828 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -2,7 +2,7 @@ import { Divider, Drawer, Grid, Link as MUILink, List } from '@material-ui/core' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import { OpenInNewSharp } from '@material-ui/icons' import type { ReactElement } from 'react' -import { Bookmark, BookOpen, DollarSign, FileText, Home, Layers, Settings } from 'react-feather' +import { Bookmark, BookOpen, Briefcase, DollarSign, FileText, Gift, Home, Layers, Settings } from 'react-feather' import { Link } from 'react-router-dom' import Logo from '../assets/logo.svg' import { config } from '../config' @@ -41,6 +41,16 @@ const navBarItems = [ path: ROUTES.SETTINGS, icon: Settings, }, + { + label: 'Account', + path: ROUTES.WALLET, + icon: Briefcase, + }, + { + label: 'Gift Wallets', + path: ROUTES.GIFT_CODES, + icon: Gift, + }, ] const drawerWidth = 300 diff --git a/src/components/SwarmDivider.tsx b/src/components/SwarmDivider.tsx new file mode 100644 index 0000000..5875a8d --- /dev/null +++ b/src/components/SwarmDivider.tsx @@ -0,0 +1,17 @@ +import { Box, Divider } from '@material-ui/core' +import { ReactElement } from 'react' + +interface Props { + my?: number + mt?: number + mb?: number + color?: string +} + +export function SwarmDivider({ my, mt, mb, color = '#cbcbcb' }: Props): ReactElement { + return ( + + + + ) +} diff --git a/src/layout/Dashboard.tsx b/src/layout/Dashboard.tsx index 89486ad..b1de345 100644 --- a/src/layout/Dashboard.tsx +++ b/src/layout/Dashboard.tsx @@ -1,12 +1,8 @@ -import { useContext, ReactElement } from 'react' +import { CircularProgress, Container } from '@material-ui/core' +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' +import { ReactElement, useContext } from 'react' import ErrorBoundary from '../components/ErrorBoundary' -import AlertVersion from '../components/AlertVersion' -import { Container, CircularProgress } from '@material-ui/core' - -import { createStyles, Theme, makeStyles } from '@material-ui/core/styles' - import SideBar from '../components/SideBar' - import { Context } from '../providers/Bee' const useStyles = makeStyles((theme: Theme) => @@ -33,7 +29,6 @@ const Dashboard = (props: Props): ReactElement => { <> - {isLoading ? (
diff --git a/src/models/BzzToken.ts b/src/models/BzzToken.ts new file mode 100644 index 0000000..f6a41d6 --- /dev/null +++ b/src/models/BzzToken.ts @@ -0,0 +1,8 @@ +import { BigNumber } from 'bignumber.js' +import { Token } from './Token' + +export class BzzToken extends Token { + constructor(amount: BigNumber | string | BigInt) { + super(amount, 16) + } +} diff --git a/src/models/DaiToken.ts b/src/models/DaiToken.ts new file mode 100644 index 0000000..1a49a4d --- /dev/null +++ b/src/models/DaiToken.ts @@ -0,0 +1,8 @@ +import { BigNumber } from 'bignumber.js' +import { Token } from './Token' + +export class DaiToken extends Token { + constructor(amount: BigNumber | string | BigInt) { + super(amount, 18) + } +} diff --git a/src/models/Token.ts b/src/models/Token.ts index 393bd83..0ce2ed7 100644 --- a/src/models/Token.ts +++ b/src/models/Token.ts @@ -13,7 +13,9 @@ export class Token { constructor(amount: BigNumber | string | BigInt, decimals: digits = BZZ_DECIMALS) { const a = makeBigNumber(amount) - if (!isInteger(a) || !POSSIBLE_DECIMALS.includes(decimals)) throw new TypeError('Not a valid token values') + if (!isInteger(a) || !POSSIBLE_DECIMALS.includes(decimals)) { + throw new TypeError(`Not a valid token values: ${amount} ${decimals}`) + } this.amount = a this.decimals = decimals @@ -59,7 +61,7 @@ export class Token { } toSignificantDigits(digits = 4): string { - const asString = this.toDecimal.toFixed(16) + const asString = this.toDecimal.toFixed(this.decimals) let indexOfSignificantDigit = -1 let reachedDecimalPoint = false @@ -78,4 +80,11 @@ export class Token { return asString.slice(0, indexOfSignificantDigit + digits) } + + minusBaseUnits(amount: string): Token { + return new Token( + this.toBigNumber.minus(new BigNumber(amount).multipliedBy(new BigNumber(10).pow(this.decimals))), + this.decimals, + ) + } } diff --git a/src/pages/accounting/PeerBalances.tsx b/src/pages/accounting/PeerBalances.tsx index 6ed9190..48016d4 100644 --- a/src/pages/accounting/PeerBalances.tsx +++ b/src/pages/accounting/PeerBalances.tsx @@ -1,11 +1,9 @@ import type { ReactElement } from 'react' - +import CashoutModal from '../../components/CashoutModal' import ExpandableList from '../../components/ExpandableList' import ExpandableListItem from '../../components/ExpandableListItem' import ExpandableListItemActions from '../../components/ExpandableListItemActions' import ExpandableListItemKey from '../../components/ExpandableListItemKey' - -import CashoutModal from '../../components/CashoutModal' import { Accounting } from '../../hooks/accounting' import type { Token } from '../../models/Token' @@ -25,7 +23,7 @@ export default function PeerBalances({ accounting, isLoadingUncashed, totalUncas {accounting?.map(({ peer, balance, received, sent, uncashedAmount, total }) => ( diff --git a/src/pages/files/SelectStamp.tsx b/src/pages/files/SelectStamp.tsx index 7fe369d..16296be 100644 --- a/src/pages/files/SelectStamp.tsx +++ b/src/pages/files/SelectStamp.tsx @@ -1,5 +1,5 @@ +import { Button, ListItemIcon, Menu, MenuItem, Typography } from '@material-ui/core' import React, { ReactElement } from 'react' -import { Button, ListItemIcon, Typography, Menu, MenuItem } from '@material-ui/core' import { EnrichedPostageBatch } from '../../providers/Stamps' interface Props { @@ -35,7 +35,7 @@ export default function SimpleMenu({ stamps, selectedStamp, setSelected }: Props selected={stamp.batchID === selectedStamp?.batchID} > {stamp.usageText} - {stamp.batchID.substr(0, 8)}[…] + {stamp.batchID.slice(0, 8)}[…] ))} diff --git a/src/pages/files/Upload.tsx b/src/pages/files/Upload.tsx index 7d6ee86..2c3937f 100644 --- a/src/pages/files/Upload.tsx +++ b/src/pages/files/Upload.tsx @@ -6,6 +6,7 @@ import { DocumentationText } from '../../components/DocumentationText' import { HistoryHeader } from '../../components/HistoryHeader' import { ProgressIndicator } from '../../components/ProgressIndicator' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' +import { META_FILE_NAME, PREVIEW_FILE_NAME } from '../../constants' import { CheckState, Context as BeeContext } from '../../providers/Bee' import { Context as IdentityContext, Identity } from '../../providers/Feeds' import { Context as FileContext } from '../../providers/File' @@ -21,7 +22,6 @@ import { PostageStampSelector } from '../stamps/PostageStampSelector' import { AssetPreview } from './AssetPreview' import { StampPreview } from './StampPreview' import { UploadActionBar } from './UploadActionBar' -import { META_FILE_NAME, PREVIEW_FILE_NAME } from '../../constants' export function Upload(): ReactElement { const [step, setStep] = useState(0) @@ -83,9 +83,9 @@ export function Upload(): ReactElement { // The website is in some directory, remove it if (idx.commonPrefix) { const substrStart = idx.commonPrefix.length - indexDocument = idx.indexPath.substr(substrStart) + indexDocument = idx.indexPath.slice(substrStart) fls = files.map(f => { - const path = (f.path as string).substr(substrStart) + const path = (f.path as string).slice(substrStart) return packageFile(f, path) }) diff --git a/src/pages/gift-code/index.tsx b/src/pages/gift-code/index.tsx new file mode 100644 index 0000000..4ac4807 --- /dev/null +++ b/src/pages/gift-code/index.tsx @@ -0,0 +1,98 @@ +import { Box, Typography } from '@material-ui/core' +import { useSnackbar } from 'notistack' +import { ReactElement, useContext, useEffect, useState } from 'react' +import { Check, X } from 'react-feather' +import { useNavigate } from 'react-router' +import ExpandableListItem from '../../components/ExpandableListItem' +import ExpandableListItemActions from '../../components/ExpandableListItemActions' +import ExpandableListItemKey from '../../components/ExpandableListItemKey' +import { HistoryHeader } from '../../components/HistoryHeader' +import { Loading } from '../../components/Loading' +import { SwarmButton } from '../../components/SwarmButton' +import { Context as BeeContext } from '../../providers/Bee' +import { Context as TopUpContext } from '../../providers/TopUp' +import { createGiftWallet } from '../../utils/desktop' +import { generateWallet } from '../../utils/identity' +import { ResolvedWallet } from '../../utils/wallet' + +export default function Index(): ReactElement { + const { giftWallets, addGiftWallet } = useContext(TopUpContext) + const { balance } = useContext(BeeContext) + + const [loading, setLoading] = useState(false) + const [balances, setBalances] = useState([]) + + useEffect(() => { + async function mapGiftWallets() { + const results = [] + for (const giftWallet of giftWallets) { + results.push(await ResolvedWallet.make(giftWallet)) + } + setBalances(results) + } + + mapGiftWallets() + }, [giftWallets]) + + const { enqueueSnackbar } = useSnackbar() + const navigate = useNavigate() + + async function onCreate() { + enqueueSnackbar('Sending funds to gift wallet...') + setLoading(true) + try { + const wallet = generateWallet() + addGiftWallet(wallet) + await createGiftWallet(wallet.getAddressString()) + enqueueSnackbar('Succesfully funded gift wallet', { variant: 'success' }) + } catch (error) { + enqueueSnackbar(`Failed to fund gift wallet: ${error}`, { variant: 'error' }) + } finally { + setLoading(false) + } + } + + function onCancel() { + navigate(-1) + } + + if (!balance) { + return + } + + return ( + <> + Invite to Swarm... + + + Generate and share a gift wallet that anyone can use to set-up their light node with Swarm Desktop. This will + use 1 XDAI and 5 BZZ from your node wallet. + + + + + + + + + + {balances.map((x, i) => ( + + + + + + + ))} + + + + Generate gift wallet + + + Cancel + + + + ) +} diff --git a/src/pages/restart/LightModeRestart.tsx b/src/pages/restart/LightModeRestart.tsx new file mode 100644 index 0000000..da1cf44 --- /dev/null +++ b/src/pages/restart/LightModeRestart.tsx @@ -0,0 +1,32 @@ +import { BeeModes } from '@ethersphere/bee-js' +import { Box, Typography } from '@material-ui/core' +import { ReactElement, useContext, useEffect, useState } from 'react' +import { useNavigate } from 'react-router' +import { Loading } from '../../components/Loading' +import { Context } from '../../providers/Bee' +import { ROUTES } from '../../routes' + +export default function Settings(): ReactElement { + const [startedAt] = useState(Date.now()) + const { apiHealth, nodeInfo } = useContext(Context) + const navigate = useNavigate() + + useEffect(() => { + if (Date.now() - startedAt < 45_000) { + return + } + + if (apiHealth && nodeInfo?.beeMode === BeeModes.LIGHT) { + navigate(ROUTES.INFO) + } + }, [startedAt, navigate, nodeInfo, apiHealth]) + + return ( + <> + + + + Your node is being upgraded to light mode... postage syncing may take up to 10 minutes. + + ) +} diff --git a/src/pages/restart/Restart.tsx b/src/pages/restart/Restart.tsx new file mode 100644 index 0000000..c51fe9a --- /dev/null +++ b/src/pages/restart/Restart.tsx @@ -0,0 +1,41 @@ +import { Box, Typography } from '@material-ui/core' +import { ReactElement, useContext, useEffect, useState } from 'react' +import { useNavigate } from 'react-router' +import { Loading } from '../../components/Loading' +import { Context } from '../../providers/Bee' +import { ROUTES } from '../../routes' + +export default function Settings(): ReactElement { + const [waited, setWaited] = useState(false) + const { apiHealth } = useContext(Context) + const navigate = useNavigate() + + useEffect(() => { + if (waited) { + return + } + + const timeout = setTimeout(() => setWaited(true), 5_000) + + return () => clearTimeout(timeout) + }, [waited]) + + useEffect(() => { + if (!waited) { + return + } + + if (apiHealth) { + navigate(ROUTES.INFO) + } + }, [navigate, waited, apiHealth]) + + return ( + <> + + + + You will be redirected automatically once your node is up and running. + + ) +} diff --git a/src/pages/rpc/Confirmation.tsx b/src/pages/rpc/Confirmation.tsx new file mode 100644 index 0000000..33ddd06 --- /dev/null +++ b/src/pages/rpc/Confirmation.tsx @@ -0,0 +1,61 @@ +import { Box, createStyles, Grid, makeStyles, Typography } from '@material-ui/core' +import { ReactElement } from 'react' +import { Battery, BatteryCharging, Check, Gift } from 'react-feather' +import { useNavigate } from 'react-router' +import ExpandableListItemActions from '../../components/ExpandableListItemActions' +import { HistoryHeader } from '../../components/HistoryHeader' +import { SwarmButton } from '../../components/SwarmButton' +import { ROUTES } from '../../routes' + +const useStyles = makeStyles(() => + createStyles({ + checkWrapper: { + background: 'rgba(0, 230, 118, 0.25)', + borderRadius: 99999, + width: '180px', + height: '180px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + }), +) + +export default function Confirmation(): ReactElement { + const navigate = useNavigate() + + const styles = useStyles() + + return ( + <> + Connect to the blockchain + + +
+ +
+
+ + Your node's RPC endpoint is set up correctly! + + + Lastly, you will need to top-up your node wallet. + + If you're not familiar with cryptocurrencies, you can start with a bank card. + + + + navigate(ROUTES.TOP_UP_BANK_CARD)}> + Get started with bank card + + navigate(ROUTES.TOP_UP_CRYPTO)}> + Use DAI + + navigate(ROUTES.TOP_UP_GIFT_CODE)}> + Use a gift code + + +
+ + ) +} diff --git a/src/pages/rpc/index.tsx b/src/pages/rpc/index.tsx new file mode 100644 index 0000000..4547ad8 --- /dev/null +++ b/src/pages/rpc/index.tsx @@ -0,0 +1,61 @@ +import { Box, Typography } from '@material-ui/core' +import { useSnackbar } from 'notistack' +import { ReactElement, useContext, useState } from 'react' +import { Check } from 'react-feather' +import { useNavigate } from 'react-router' +import { HistoryHeader } from '../../components/HistoryHeader' +import { SwarmButton } from '../../components/SwarmButton' +import { SwarmTextInput } from '../../components/SwarmTextInput' +import { Context } from '../../providers/TopUp' +import { ROUTES } from '../../routes' +import { Rpc } from '../../utils/rpc' + +export default function Index(): ReactElement { + const { jsonRpcProvider, setJsonRpcProvider } = useContext(Context) + + const [provider, setProvider] = useState(jsonRpcProvider) + + const { enqueueSnackbar } = useSnackbar() + const navigate = useNavigate() + + async function onSubmit() { + try { + await Rpc.eth_getBlockByNumber(provider) + enqueueSnackbar('Connected to RPC provider successfully.', { variant: 'success' }) + setJsonRpcProvider(provider) + navigate(ROUTES.CONFIRMATION) + } catch (error) { + enqueueSnackbar('Could not connect to RPC provider.', { variant: 'error' }) + } + } + + return ( + <> + Connect to the blockchain + + Set up RPC endpoint + + + + To connect to and retrieve data from the blockchain, you'll need to connect to a publicly-provided node + via the node's RPC endpoint. If you're not familiar with this, you may use{' '} + + https://getblock.io/ + + . + + + + setProvider(event.target.value)} + defaultValue={jsonRpcProvider} + /> + + + Connect + + + ) +} diff --git a/src/pages/top-up/BankCardTopUpIndex.tsx b/src/pages/top-up/BankCardTopUpIndex.tsx new file mode 100644 index 0000000..f02c63f --- /dev/null +++ b/src/pages/top-up/BankCardTopUpIndex.tsx @@ -0,0 +1,24 @@ +import { Typography } from '@material-ui/core' +import { ReactElement } from 'react' +import Index from '.' +import { ROUTES } from '../../routes' + +export function BankCardTopUpIndex(): ReactElement { + return ( + + It's recommended to buy an amount equivalent to 5 to 10 EUR maximum. If you're not familiar with + cryptocurrencies, you may use{' '} + + https://ramp.network/buy/ + + . + + } + next={ROUTES.TOP_UP_BANK_CARD_SWAP} + /> + ) +} diff --git a/src/pages/top-up/CryptoTopUpIndex.tsx b/src/pages/top-up/CryptoTopUpIndex.tsx new file mode 100644 index 0000000..07cfbcd --- /dev/null +++ b/src/pages/top-up/CryptoTopUpIndex.tsx @@ -0,0 +1,23 @@ +import { Typography } from '@material-ui/core' +import { ReactElement } from 'react' +import Index from '.' +import { ROUTES } from '../../routes' + +export function CryptoTopUpIndex(): ReactElement { + return ( + + For security reasons it is recommended to send maximum 5 to 10 xDAI. To get xDAI from DAI you may use{' '} + + https://bridge.xdaichain.com/ + + . + + } + next={ROUTES.TOP_UP_CRYPTO_SWAP} + /> + ) +} diff --git a/src/pages/top-up/GiftCardFund.tsx b/src/pages/top-up/GiftCardFund.tsx new file mode 100644 index 0000000..c761002 --- /dev/null +++ b/src/pages/top-up/GiftCardFund.tsx @@ -0,0 +1,107 @@ +import { Box, Typography } from '@material-ui/core' +import { useSnackbar } from 'notistack' +import { ReactElement, useContext, useEffect, useState } from 'react' +import { ArrowDown, Check } from 'react-feather' +import { useNavigate, useParams } from 'react-router' +import ExpandableListItem from '../../components/ExpandableListItem' +import ExpandableListItemKey from '../../components/ExpandableListItemKey' +import { HistoryHeader } from '../../components/HistoryHeader' +import { Loading } from '../../components/Loading' +import { ProgressIndicator } from '../../components/ProgressIndicator' +import { SwarmButton } from '../../components/SwarmButton' +import { SwarmDivider } from '../../components/SwarmDivider' +import { Context as BeeContext } from '../../providers/Bee' +import { Context as TopUpContext } from '../../providers/TopUp' +import { ROUTES } from '../../routes' +import { sleepMs } from '../../utils' +import { restartBeeNode, upgradeToLightNode } from '../../utils/desktop' +import { ResolvedWallet } from '../../utils/wallet' + +export function GiftCardFund(): ReactElement { + const { nodeAddresses, balance } = useContext(BeeContext) + const { jsonRpcProvider } = useContext(TopUpContext) + + const [loading, setLoading] = useState(false) + const [wallet, setWallet] = useState(null) + + const { privateKeyString } = useParams() + + const { enqueueSnackbar } = useSnackbar() + const navigate = useNavigate() + + useEffect(() => { + if (!privateKeyString) { + return + } + + ResolvedWallet.make(privateKeyString).then(setWallet) + }, [privateKeyString]) + + if (!wallet || !balance) { + return + } + + async function onFund() { + if (!wallet || !nodeAddresses) { + return + } + + setLoading(true) + + try { + await wallet.transfer(nodeAddresses.ethereum) + enqueueSnackbar('Successfully funded node, restarting...', { variant: 'success' }) + await sleepMs(5_000) + await upgradeToLightNode(jsonRpcProvider) + await restartBeeNode() + navigate(ROUTES.RESTART_LIGHT) + } catch (error) { + enqueueSnackbar(`Failed to fund/restart node: ${error}`, { variant: 'error' }) + } finally { + setLoading(false) + } + } + + return ( + <> + Top-up with gift code + + + + + Send funds to your Bee node + + + + Deposit all the funds from the gift wallet to your node wallet address. You can use the button below to + transfer all funds to your node. + + + + + + + + + + + + + + + + + + + + + + + + + + Send all funds to your node + + + ) +} diff --git a/src/pages/top-up/GiftCardTopUpIndex.tsx b/src/pages/top-up/GiftCardTopUpIndex.tsx new file mode 100644 index 0000000..2bd26e8 --- /dev/null +++ b/src/pages/top-up/GiftCardTopUpIndex.tsx @@ -0,0 +1,71 @@ +import { Box, Typography } from '@material-ui/core' +import { useSnackbar } from 'notistack' +import { ReactElement, useState } from 'react' +import { ArrowRight } from 'react-feather' +import { useNavigate } from 'react-router' +import { HistoryHeader } from '../../components/HistoryHeader' +import { ProgressIndicator } from '../../components/ProgressIndicator' +import { SwarmButton } from '../../components/SwarmButton' +import { SwarmDivider } from '../../components/SwarmDivider' +import { SwarmTextInput } from '../../components/SwarmTextInput' +import { BzzToken } from '../../models/BzzToken' +import { DaiToken } from '../../models/DaiToken' +import { ROUTES } from '../../routes' +import { getWalletFromPrivateKeyString } from '../../utils/identity' +import { Rpc } from '../../utils/rpc' + +export function GiftCardTopUpIndex(): ReactElement { + const [loading, setLoading] = useState(false) + const [giftCode, setGiftCode] = useState('') + + const { enqueueSnackbar } = useSnackbar() + const navigate = useNavigate() + + async function onProceed() { + setLoading(true) + try { + const wallet = getWalletFromPrivateKeyString(giftCode) + const dai = new DaiToken(await Rpc._eth_getBalance(wallet.getAddressString())) + const bzz = new BzzToken(await Rpc._eth_getBalanceERC20(wallet.getAddressString())) + + if (dai.toDecimal.lt(0.001) || bzz.toDecimal.lt(0.001)) { + throw Error('Gift wallet does not have enough funds') + } + enqueueSnackbar('Successfully verified gift wallet', { variant: 'success' }) + navigate(ROUTES.TOP_UP_GIFT_CODE_FUND.replace(':privateKeyString', giftCode)) + } catch (error) { + enqueueSnackbar(`Gift wallet could not be verified: ${error}`, { variant: 'error' }) + } finally { + setLoading(false) + } + } + + return ( + <> + Top-up with gift code + + + + + Please paste your gift code below + + + A gift code is a unique key to a gift wallet that you can use to fund your node. Please don't share your + gift code as it can only be used once. + + + + { + setGiftCode(event.target.value) + }} + /> + + + Proceed + + + ) +} diff --git a/src/pages/top-up/Swap.tsx b/src/pages/top-up/Swap.tsx new file mode 100644 index 0000000..e43e3dd --- /dev/null +++ b/src/pages/top-up/Swap.tsx @@ -0,0 +1,127 @@ +import { Box, Typography } from '@material-ui/core' +import { useSnackbar } from 'notistack' +import { ReactElement, useContext, useState } from 'react' +import { ArrowDown, Check } from 'react-feather' +import { useNavigate } from 'react-router' +import ExpandableListItem from '../../components/ExpandableListItem' +import ExpandableListItemActions from '../../components/ExpandableListItemActions' +import ExpandableListItemKey from '../../components/ExpandableListItemKey' +import { HistoryHeader } from '../../components/HistoryHeader' +import { Loading } from '../../components/Loading' +import { SwarmButton } from '../../components/SwarmButton' +import { SwarmDivider } from '../../components/SwarmDivider' +import { SwarmTextInput } from '../../components/SwarmTextInput' +import { BzzToken } from '../../models/BzzToken' +import { DaiToken } from '../../models/DaiToken' +import { Context as BeeContext } from '../../providers/Bee' +import { Context as TopUpContext } from '../../providers/TopUp' +import { ROUTES } from '../../routes' +import { sleepMs } from '../../utils' +import { performSwap, restartBeeNode, upgradeToLightNode } from '../../utils/desktop' +import { TopUpProgressIndicator } from './TopUpProgressIndicator' + +interface Props { + header: string +} + +export function Swap({ header }: Props): ReactElement { + const [loading, setLoading] = useState(false) + const [hasSwapped, setSwapped] = useState(false) + + const { jsonRpcProvider } = useContext(TopUpContext) + const { balance } = useContext(BeeContext) + + const navigate = useNavigate() + const { enqueueSnackbar } = useSnackbar() + + if (!balance) { + return + } + + const daiToSwap = balance.dai.minusBaseUnits('1') + + const daiAfterSwap = new DaiToken(balance.dai.toBigNumber.minus(daiToSwap.toBigNumber)) + const bzzAfterSwap = new BzzToken(daiToSwap.toBigNumber.dividedToIntegerBy(200)) + + async function onSwap() { + if (hasSwapped) { + return + } + setLoading(true) + setSwapped(true) + try { + await performSwap(daiToSwap.toString) + enqueueSnackbar('Successfully swapped, restarting...', { variant: 'success' }) + await sleepMs(5_000) + await upgradeToLightNode(jsonRpcProvider) + await restartBeeNode() + navigate(ROUTES.RESTART_LIGHT) + enqueueSnackbar('Upgraded to light node', { variant: 'success' }) + } catch (error) { + enqueueSnackbar(`Failed to swap: ${error}`, { variant: 'error' }) + } finally { + setLoading(false) + } + } + + return ( + <> + {header} + + + + + Swap some xDAI to BZZ + + + + You need to swap xDAI to BZZ in order to use Swarm. Make sure to keep at least 1 xDAI in order to pay for + transaction costs on the network. + + + + + + Your current balance is {balance.dai.toSignificantDigits(4)} xDAI and {balance.bzz.toSignificantDigits(4)}{' '} + BZZ. + + + + false} + /> + + + + + + + + + + + + + + + + Swap Now + + + + ) +} diff --git a/src/pages/top-up/TopUpProgressIndicator.tsx b/src/pages/top-up/TopUpProgressIndicator.tsx new file mode 100644 index 0000000..0de6465 --- /dev/null +++ b/src/pages/top-up/TopUpProgressIndicator.tsx @@ -0,0 +1,10 @@ +import { ReactElement } from 'react' +import { ProgressIndicator } from '../../components/ProgressIndicator' + +interface Props { + index: number +} + +export function TopUpProgressIndicator({ index }: Props): ReactElement { + return +} diff --git a/src/pages/top-up/index.tsx b/src/pages/top-up/index.tsx new file mode 100644 index 0000000..556f09b --- /dev/null +++ b/src/pages/top-up/index.tsx @@ -0,0 +1,56 @@ +import { Box, Grid, Typography } from '@material-ui/core' +import { ReactElement, useContext } from 'react' +import { Check } from 'react-feather' +import { useNavigate } from 'react-router' +import ExpandableListItem from '../../components/ExpandableListItem' +import ExpandableListItemKey from '../../components/ExpandableListItemKey' +import { HistoryHeader } from '../../components/HistoryHeader' +import { Loading } from '../../components/Loading' +import { SwarmButton } from '../../components/SwarmButton' +import { SwarmDivider } from '../../components/SwarmDivider' +import { Context } from '../../providers/Bee' +import { TopUpProgressIndicator } from './TopUpProgressIndicator' + +interface Props { + header: string + title: string + p: ReactElement + next: string +} + +export default function Index({ header, title, p, next }: Props): ReactElement { + const { nodeAddresses, balance } = useContext(Context) + const navigate = useNavigate() + + if (!balance || !nodeAddresses) { + return + } + + const disabled = balance.dai.toDecimal.lte(1) + + return ( + <> + {header} + + + + + {title} + + {p} + + + + + + + + + navigate(next)} disabled={disabled}> + Proceed + + {disabled ? Please deposit xDAI to the address above in order to proceed. : null} + + + ) +} diff --git a/src/providers/Bee.tsx b/src/providers/Bee.tsx index b4b692d..280715c 100644 --- a/src/providers/Bee.tsx +++ b/src/providers/Bee.tsx @@ -15,14 +15,9 @@ import { engines } from '../../package.json' import { useLatestBeeRelease } from '../hooks/apiHooks' import { Token } from '../models/Token' import type { Balance, ChequebookBalance, Settlements } from '../types' -import { Rpc } from '../utils/rpc' +import { WalletAddress } from '../utils/wallet' import { Context as SettingsContext } from './Settings' -interface RpcBalance { - bzz: Token - xdai: Token -} - export enum CheckState { OK = 'OK', WARNING = 'Warning', @@ -46,7 +41,7 @@ interface Status { interface ContextInterface { status: Status - balance: RpcBalance + balance: WalletAddress | null latestPublishedVersion?: string latestUserVersion?: string latestUserVersionExact?: string @@ -84,10 +79,7 @@ const initialValues: ContextInterface = { topology: { isEnabled: false, checkState: CheckState.ERROR }, chequebook: { isEnabled: false, checkState: CheckState.ERROR }, }, - balance: { - bzz: new Token('0', 16), - xdai: new Token('0', 18), - }, + balance: null, latestPublishedVersion: undefined, latestUserVersion: undefined, latestUserVersionExact: undefined, @@ -204,8 +196,7 @@ export function Provider({ children }: Props): ReactElement { const [peerCheques, setPeerCheques] = useState(null) const [settlements, setSettlements] = useState(null) const [chainState, setChainState] = useState(null) - const [bzz, setBzz] = useState(initialValues.balance.bzz) - const [xdai, setXdai] = useState(initialValues.balance.xdai) + const [walletAddress, setWalletAddress] = useState(initialValues.balance) const { latestBeeRelease } = useLatestBeeRelease() @@ -247,20 +238,16 @@ export function Provider({ children }: Props): ReactElement { useEffect(() => { if (nodeAddresses?.ethereum) { - // debounced calls - const xdai = Rpc.eth_getBalance(nodeAddresses.ethereum) - const bzz = Rpc.eth_getBalanceERC20(nodeAddresses.ethereum) - - if (xdai?.then) { - xdai.then(balance => setXdai(new Token(balance, 18))) - } - - if (bzz?.then) { - bzz.then(balance => setBzz(new Token(balance, 16))) - } + WalletAddress.make(nodeAddresses.ethereum).then(setWalletAddress) } }, [nodeAddresses]) + useEffect(() => { + const interval = setInterval(() => walletAddress?.refresh().then(setWalletAddress), 30_000) + + return () => clearInterval(interval) + }, [walletAddress]) + const refresh = async () => { // Don't want to refresh when already refreshing if (isRefreshing) return @@ -417,10 +404,7 @@ export function Provider({ children }: Props): ReactElement { chequebookBalance, error, ), - balance: { - xdai, - bzz, - }, + balance: walletAddress, latestUserVersion, latestUserVersionExact, latestPublishedVersion, diff --git a/src/providers/Settings.tsx b/src/providers/Settings.tsx index 036a642..3680030 100644 --- a/src/providers/Settings.tsx +++ b/src/providers/Settings.tsx @@ -55,8 +55,16 @@ export function Provider({ const [desktopApiKey, setDesktopApiKey] = useState(initialValues.desktopApiKey) const { config, isLoading, error } = useGetBeeConfig() - const url = config?.['api-addr'] || beeApiUrl || apiUrl - const debugUrl = config?.['debug-api-addr'] || beeDebugApiUrl || apiDebugUrl + function makeHttpUrl(string: string): string { + if (!string.startsWith('http')) { + return `http://${string}` + } + + return string + } + + const url = makeHttpUrl(config?.['api-addr'] || beeApiUrl || apiUrl) + const debugUrl = makeHttpUrl(config?.['debug-api-addr'] || beeDebugApiUrl || apiDebugUrl) useEffect(() => { const urlSearchParams = new URLSearchParams(window.location.search) diff --git a/src/providers/TopUp.tsx b/src/providers/TopUp.tsx new file mode 100644 index 0000000..bd3a051 --- /dev/null +++ b/src/providers/TopUp.tsx @@ -0,0 +1,73 @@ +import Wallet from 'ethereumjs-wallet' +import { createContext, ReactElement, useEffect, useState } from 'react' +import { setJsonRpcInDesktop } from '../utils/desktop' +import { getWalletFromPrivateKeyString } from '../utils/identity' + +const LocalStorageKeys = { + jsonRpcProvider: 'json-rpc-provider', + depositWallet: 'deposit-wallet', + giftWallets: 'gift-wallets', + invitation: 'invitation', +} + +interface ContextInterface { + jsonRpcProvider: string + giftWallets: Wallet[] + setJsonRpcProvider: (jsonRpcProvider: string) => void + addGiftWallet: (wallet: Wallet) => void +} + +const initialValues: ContextInterface = { + jsonRpcProvider: '', + giftWallets: [], + setJsonRpcProvider: () => {}, // eslint-disable-line + addGiftWallet: () => {}, // eslint-disable-line +} + +export const Context = createContext(initialValues) +export const Consumer = Context.Consumer + +interface Props { + children: ReactElement +} + +export function Provider({ children }: Props): ReactElement { + const [jsonRpcProvider, setJsonRpcProvider] = useState( + localStorage.getItem('json-rpc-provider') || initialValues.jsonRpcProvider, + ) + const [giftWallets, setGiftWallets] = useState(initialValues.giftWallets) + + useEffect(() => { + const existingGiftWallets = localStorage.getItem(LocalStorageKeys.giftWallets) + + if (existingGiftWallets) { + setGiftWallets(JSON.parse(existingGiftWallets).map(getWalletFromPrivateKeyString)) + } + }, []) + + function setAndPersistJsonRpcProvider(jsonRpcProvider: string) { + localStorage.setItem(LocalStorageKeys.jsonRpcProvider, jsonRpcProvider) + setJsonRpcProvider(jsonRpcProvider) + // eslint-disable-next-line no-console + setJsonRpcInDesktop(jsonRpcProvider).catch(console.error) + } + + function addGiftWallet(wallet: Wallet) { + const newArray = [...giftWallets, wallet] + localStorage.setItem(LocalStorageKeys.giftWallets, JSON.stringify(newArray.map(x => x.getPrivateKeyString()))) + setGiftWallets(newArray) + } + + return ( + + {children} + + ) +} diff --git a/src/routes.tsx b/src/routes.tsx index ad0aa5e..165fed3 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -9,11 +9,21 @@ import { Download } from './pages/files/Download' import { Share } from './pages/files/Share' import { Upload } from './pages/files/Upload' import { UploadLander } from './pages/files/UploadLander' +import GiftCards from './pages/gift-code' import Info from './pages/info' +import LightModeRestart from './pages/restart/LightModeRestart' +import Restart from './pages/restart/Restart' +import Wallet from './pages/rpc' +import Confirmation from './pages/rpc/Confirmation' import Settings from './pages/settings' import Stamps from './pages/stamps' import { CreatePostageStampPage } from './pages/stamps/CreatePostageStampPage' import Status from './pages/status' +import { BankCardTopUpIndex } from './pages/top-up/BankCardTopUpIndex' +import { CryptoTopUpIndex } from './pages/top-up/CryptoTopUpIndex' +import { GiftCardFund } from './pages/top-up/GiftCardFund' +import { GiftCardTopUpIndex } from './pages/top-up/GiftCardTopUpIndex' +import { Swap } from './pages/top-up/Swap' export enum ROUTES { INFO = '/', @@ -31,6 +41,17 @@ export enum ROUTES { FEEDS_NEW = '/feeds/new', FEEDS_UPDATE = '/feeds/update/:hash', FEEDS_PAGE = '/feeds/:uuid', + WALLET = '/wallet', + CONFIRMATION = '/wallet/confirmation', + TOP_UP_CRYPTO = '/top-up/crypto', + TOP_UP_CRYPTO_SWAP = '/top-up/crypto/swap', + TOP_UP_BANK_CARD = '/top-up/bank-card', + TOP_UP_BANK_CARD_SWAP = '/top-up/bank-card/swap', + TOP_UP_GIFT_CODE = '/top-up/gift-code', + TOP_UP_GIFT_CODE_FUND = '/top-up/gift-code/fund/:privateKeyString', + GIFT_CODES = '/gift-codes', + RESTART = '/restart', + RESTART_LIGHT = '/light-mode-restart', } const BaseRouter = (): ReactElement => ( @@ -49,6 +70,17 @@ const BaseRouter = (): ReactElement => ( } /> } /> } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> ) diff --git a/src/utils/bzz-contract-interface.ts b/src/utils/bzz-contract-interface.ts new file mode 100644 index 0000000..2f22707 --- /dev/null +++ b/src/utils/bzz-contract-interface.ts @@ -0,0 +1,25 @@ +export const bzzContractInterface = [ + { + type: 'function', + stateMutability: 'nonpayable', + payable: false, + outputs: [ + { + type: 'bool', + name: '', + }, + ], + name: 'transfer', + inputs: [ + { + type: 'address', + name: '_to', + }, + { + type: 'uint256', + name: '_value', + }, + ], + constant: false, + }, +] diff --git a/src/utils/desktop.ts b/src/utils/desktop.ts index 388cb75..ddc267c 100644 --- a/src/utils/desktop.ts +++ b/src/utils/desktop.ts @@ -26,6 +26,12 @@ export async function upgradeToLightNode(rpcProvider: string): Promise { }) } +export async function setJsonRpcInDesktop(value: string): Promise { + await updateDesktopConfiguration({ + 'swap-endpoint': value, + }) +} + async function updateDesktopConfiguration(values: Record): Promise { await postJson(`http://${getDesktopHost()}/config`, values) } @@ -34,6 +40,14 @@ export async function restartBeeNode(): Promise { await postJson(`http://${getDesktopHost()}/restart`) } +export async function createGiftWallet(address: string): Promise { + await postJson(`http://${getDesktopHost()}/gift-wallet/${address}`) +} + +export async function performSwap(daiAmount: string): Promise { + await postJson(`http://${getDesktopHost()}/swap`, { dai: daiAmount }) +} + function getDesktopHost(): string { return window.location.host } diff --git a/src/utils/identity.ts b/src/utils/identity.ts index d83127a..442a9b5 100644 --- a/src/utils/identity.ts +++ b/src/utils/identity.ts @@ -79,9 +79,11 @@ function getWalletFromIdentity(identity: Identity, password?: string): Promise { - return type === 'PRIVATE_KEY' - ? Wallet.fromPrivateKey(Buffer.from(trimHexString(data), 'hex')) - : await Wallet.fromV3(data, password as string) + return type === 'PRIVATE_KEY' ? getWalletFromPrivateKeyString(data) : await Wallet.fromV3(data, password as string) +} + +export function getWalletFromPrivateKeyString(privateKey: string): Wallet { + return Wallet.fromPrivateKey(Buffer.from(trimHexString(privateKey), 'hex')) } export async function updateFeed( diff --git a/src/utils/rpc.ts b/src/utils/rpc.ts index 04e701a..d9bb5c4 100644 --- a/src/utils/rpc.ts +++ b/src/utils/rpc.ts @@ -1,14 +1,15 @@ import { debounce } from '@material-ui/core' import axios from 'axios' -import { Contract, providers } from 'ethers' +import { Contract, providers, Wallet } from 'ethers' +import { bzzContractInterface } from './bzz-contract-interface' -const PROVIDER = 'https://gno.getblock.io/mainnet/?api_key=d7b92d96-9784-49a8-a800-b3edd1647fc7' +export const JSON_RPC_PROVIDER = 'https://gno.getblock.io/mainnet/?api_key=d7b92d96-9784-49a8-a800-b3edd1647fc7' async function eth_getBalance(address: string): Promise { if (!address.startsWith('0x')) { address = `0x${address}` } - const response = await axios(PROVIDER, { + const response = await axios(JSON_RPC_PROVIDER, { method: 'POST', headers: { 'content-type': 'application/json', @@ -24,6 +25,23 @@ async function eth_getBalance(address: string): Promise { return response.data.result } +async function eth_getBlockByNumber(provider = JSON_RPC_PROVIDER): Promise { + const response = await axios(provider, { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + data: { + jsonrpc: '2.0', + method: 'eth_getBlockByNumber', + params: ['latest', false], + id: 1, + }, + }) + + return response.data.result +} + const partialERC20tokenABI = [ { constant: true, @@ -45,7 +63,7 @@ const partialERC20tokenABI = [ }, ] -const provider = new providers.JsonRpcProvider(PROVIDER) +const provider = new providers.JsonRpcProvider(JSON_RPC_PROVIDER) async function eth_getBalanceERC20( address: string, @@ -60,7 +78,54 @@ async function eth_getBalanceERC20( return balance.toString() } +interface TransferResponse { + transaction: providers.TransactionResponse + receipt: providers.TransactionReceipt +} + +export async function sendNativeTransaction( + privateKey: string, + to: string, + value: string, + jsonRpcProvider: string, +): Promise { + const signer = await makeReadySigner(privateKey, jsonRpcProvider) + const gasPrice = await signer.getGasPrice() + const transaction = await signer.sendTransaction({ to, value, gasPrice }) + const receipt = await transaction.wait(1) + + return { transaction, receipt } +} + +export async function sendBzzTransaction( + privateKey: string, + to: string, + value: string, + jsonRpcProvider: string, +): Promise { + const signer = await makeReadySigner(privateKey, jsonRpcProvider) + const gasPrice = await signer.getGasPrice() + const bzz = new Contract('0xdBF3Ea6F5beE45c02255B2c26a16F300502F68da', bzzContractInterface, signer) + const transaction = await bzz.transfer(to, value, { gasPrice }) + const receipt = await transaction.wait(1) + + return { transaction, receipt } +} + +async function makeReadySigner(privateKey: string, jsonRpcProvider: string) { + const provider = new providers.JsonRpcProvider(jsonRpcProvider, 100) + await provider.ready + const signer = new Wallet(privateKey, provider) + + return signer +} + export const Rpc = { + sendNativeTransaction, + sendBzzTransaction, + _eth_getBalance: eth_getBalance, + _eth_getBalanceERC20: eth_getBalanceERC20, eth_getBalance: debounce(eth_getBalance, 1_000), eth_getBalanceERC20: debounce(eth_getBalanceERC20, 1_000), + eth_getBlockByNumber, } diff --git a/src/utils/wallet.ts b/src/utils/wallet.ts new file mode 100644 index 0000000..bcef241 --- /dev/null +++ b/src/utils/wallet.ts @@ -0,0 +1,72 @@ +import Wallet from 'ethereumjs-wallet' +import { sleepMs } from '.' +import { BzzToken } from '../models/BzzToken' +import { DaiToken } from '../models/DaiToken' +import { getWalletFromPrivateKeyString } from './identity' +import { Rpc } from './rpc' + +export class WalletAddress { + private constructor(public address: string, public bzz: BzzToken, public dai: DaiToken) {} + + static async make(address: string): Promise { + const bzz = new BzzToken(await Rpc._eth_getBalanceERC20(address)) + const dai = new DaiToken(await Rpc._eth_getBalance(address)) + + return new WalletAddress(address, bzz, dai) + } + + public async refresh(): Promise { + this.bzz = new BzzToken(await Rpc._eth_getBalanceERC20(this.address)) + this.dai = new DaiToken(await Rpc._eth_getBalance(this.address)) + + return this + } +} + +export class ResolvedWallet { + public address: string + public privateKey: string + + private constructor(public wallet: Wallet, public bzz: BzzToken, public dai: DaiToken) { + this.address = wallet.getAddressString() + this.privateKey = wallet.getPrivateKeyString() + } + + static async make(privateKeyOrWallet: string | Wallet): Promise { + const wallet = + typeof privateKeyOrWallet === 'string' ? getWalletFromPrivateKeyString(privateKeyOrWallet) : privateKeyOrWallet + const address = wallet.getAddressString() + const bzz = new BzzToken(await Rpc._eth_getBalanceERC20(address)) + const dai = new DaiToken(await Rpc._eth_getBalance(address)) + + return new ResolvedWallet(wallet, bzz, dai) + } + + public async refresh(): Promise { + this.bzz = new BzzToken(await Rpc._eth_getBalanceERC20(this.address)) + this.dai = new DaiToken(await Rpc._eth_getBalance(this.address)) + + return this + } + + public async transfer( + destination: string, + jsonRpcProvider = 'https://gno.getblock.io/mainnet/?api_key=d7b92d96-9784-49a8-a800-b3edd1647fc7', + ): Promise { + const DUMMY_GAS_PRICE = '300000000000000' + + if (this.bzz.toDecimal.gt(0.1)) { + await Rpc.sendBzzTransaction(this.privateKey, destination, this.bzz.toString, jsonRpcProvider) + await sleepMs(5_000) + } + + if (this.dai.toBigNumber.gt(DUMMY_GAS_PRICE)) { + await Rpc.sendNativeTransaction( + this.privateKey, + destination, + this.dai.toBigNumber.minus(DUMMY_GAS_PRICE).toString(), + jsonRpcProvider, + ) + } + } +}