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,
+ )
+ }
+ }
+}