diff --git a/src/App.tsx b/src/App.tsx index 0336b45..a7a7171 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,7 @@ 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 { Provider as BalanceProvider } from './providers/WalletBalance' import BaseRouter from './routes' import { theme } from './theme' @@ -45,26 +46,28 @@ const App = ({ > - - - - - - - - <> - - - - - - - - - - - - + + + + + + + + + <> + + + + + + + + + + + + + diff --git a/src/pages/gift-code/index.tsx b/src/pages/gift-code/index.tsx index 3f56c8f..c4944b1 100644 --- a/src/pages/gift-code/index.tsx +++ b/src/pages/gift-code/index.tsx @@ -1,5 +1,5 @@ -import { Box, Tooltip, Typography } from '@material-ui/core' import { BZZ, DAI } from '@ethersphere/bee-js' +import { Box, Tooltip, Typography } from '@material-ui/core' import { Wallet } from 'ethers' import { useSnackbar } from 'notistack' import { ReactElement, useContext, useEffect, useState } from 'react' @@ -12,9 +12,9 @@ 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 SettingsContext } from '../../providers/Settings' import { Context as TopUpContext } from '../../providers/TopUp' +import { Context as BalanceProvider } from '../../providers/WalletBalance' import { createGiftWallet } from '../../utils/desktop' import { ResolvedWallet } from '../../utils/wallet' @@ -24,7 +24,7 @@ const GIFT_WALLET_FUND_BZZ_AMOUNT = BZZ.fromDecimalString('0.5') export default function Index(): ReactElement { const { giftWallets, addGiftWallet } = useContext(TopUpContext) const { rpcProvider, desktopUrl } = useContext(SettingsContext) - const { walletBalance } = useContext(BeeContext) + const { balance } = useContext(BalanceProvider) const [loading, setLoading] = useState(false) const [balances, setBalances] = useState([]) @@ -67,13 +67,12 @@ export default function Index(): ReactElement { navigate(-1) } - if (!walletBalance) { + if (!balance) { return } const notEnoughFundsCheck = - walletBalance.nativeTokenBalance.lte(GIFT_WALLET_FUND_DAI_AMOUNT) || - walletBalance.bzzBalance.lt(GIFT_WALLET_FUND_BZZ_AMOUNT) + balance.dai.lte(GIFT_WALLET_FUND_DAI_AMOUNT) || balance.bzz.lt(GIFT_WALLET_FUND_BZZ_AMOUNT) return ( <> @@ -86,13 +85,10 @@ export default function Index(): ReactElement { - + - + {balances.map((x, i) => ( diff --git a/src/pages/top-up/Balance.tsx b/src/pages/top-up/Balance.tsx index 778052b..e2ec2e1 100644 --- a/src/pages/top-up/Balance.tsx +++ b/src/pages/top-up/Balance.tsx @@ -1,5 +1,5 @@ -import { Box, Grid, Typography } from '@material-ui/core' import { DAI } from '@ethersphere/bee-js' +import { Box, Grid, Typography } from '@material-ui/core' import { ReactElement, useContext } from 'react' import { useNavigate } from 'react-router' import Check from 'remixicon-react/CheckLineIcon' @@ -10,6 +10,7 @@ import { Loading } from '../../components/Loading' import { SwarmButton } from '../../components/SwarmButton' import { SwarmDivider } from '../../components/SwarmDivider' import { Context } from '../../providers/Bee' +import { Context as BalanceProvider } from '../../providers/WalletBalance' import { TopUpProgressIndicator } from './TopUpProgressIndicator' const MINIMUM_XDAI = DAI.fromDecimalString('0.5') @@ -22,14 +23,15 @@ interface Props { } export default function Index({ header, title, p, next }: Props): ReactElement { - const { nodeAddresses, walletBalance } = useContext(Context) + const { nodeAddresses } = useContext(Context) + const { balance } = useContext(BalanceProvider) const navigate = useNavigate() - if (!walletBalance || !nodeAddresses) { + if (!balance || !nodeAddresses) { return } - const disabled = walletBalance.nativeTokenBalance.lt(MINIMUM_XDAI) + const disabled = balance.dai.lt(MINIMUM_XDAI) return ( <> @@ -43,10 +45,10 @@ export default function Index({ header, title, p, next }: Props): ReactElement { {p} - + - + navigate(next)} disabled={disabled}> diff --git a/src/pages/top-up/GiftCardFund.tsx b/src/pages/top-up/GiftCardFund.tsx index 3e2d455..dc86e57 100644 --- a/src/pages/top-up/GiftCardFund.tsx +++ b/src/pages/top-up/GiftCardFund.tsx @@ -1,5 +1,5 @@ -import { Box, Typography } from '@material-ui/core' import { BeeModes } from '@ethersphere/bee-js' +import { Box, Typography } from '@material-ui/core' import { useSnackbar } from 'notistack' import { ReactElement, useContext, useEffect, useState } from 'react' import { useNavigate, useParams } from 'react-router' @@ -14,14 +14,16 @@ import { SwarmButton } from '../../components/SwarmButton' import { SwarmDivider } from '../../components/SwarmDivider' import { Context as BeeContext } from '../../providers/Bee' import { Context as SettingsContext } from '../../providers/Settings' +import { Context as BalanceProvider } from '../../providers/WalletBalance' 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, nodeInfo, walletBalance } = useContext(BeeContext) + const { nodeAddresses, nodeInfo } = useContext(BeeContext) const { isDesktop, desktopUrl, rpcProvider, rpcProviderUrl } = useContext(SettingsContext) + const { balance } = useContext(BalanceProvider) const [loading, setLoading] = useState(false) const [wallet, setWallet] = useState(null) @@ -39,7 +41,7 @@ export function GiftCardFund(): ReactElement { ResolvedWallet.make(privateKeyString, rpcProvider).then(setWallet) }, [privateKeyString, rpcProvider]) - if (!wallet || !walletBalance) { + if (!wallet || !balance) { return } @@ -94,7 +96,7 @@ export function GiftCardFund(): ReactElement { - + @@ -113,13 +115,10 @@ export function GiftCardFund(): ReactElement { /> - + - + {canUpgradeToLightNode ? 'Send all funds to your node and Upgrade' : 'Send all funds to your node'} diff --git a/src/pages/top-up/Swap.tsx b/src/pages/top-up/Swap.tsx index 8ef82d6..238eb50 100644 --- a/src/pages/top-up/Swap.tsx +++ b/src/pages/top-up/Swap.tsx @@ -1,5 +1,5 @@ -import { Box, Typography } from '@material-ui/core' import { BeeModes, BZZ, DAI } from '@ethersphere/bee-js' +import { Box, Typography } from '@material-ui/core' import { useSnackbar } from 'notistack' import { ReactElement, useContext, useEffect, useState } from 'react' import { useNavigate } from 'react-router' @@ -15,6 +15,7 @@ import { SwarmDivider } from '../../components/SwarmDivider' import { SwarmTextInput } from '../../components/SwarmTextInput' import { Context as BeeContext } from '../../providers/Bee' import { Context as SettingsContext } from '../../providers/Settings' +import { Context as BalanceProvider } from '../../providers/WalletBalance' import { ROUTES } from '../../routes' import { sleepMs } from '../../utils' import { isSwapError, SwapError, wrapWithSwapError } from '../../utils/SwapError' @@ -48,7 +49,8 @@ export function Swap({ header }: Props): ReactElement { const [daiAfterSwap, setDaiAfterSwap] = useState(null) const { rpcProviderUrl, isDesktop, desktopUrl } = useContext(SettingsContext) - const { nodeAddresses, nodeInfo, walletBalance } = useContext(BeeContext) + const { nodeAddresses, nodeInfo } = useContext(BeeContext) + const { balance } = useContext(BalanceProvider) const navigate = useNavigate() const { enqueueSnackbar } = useSnackbar() @@ -61,20 +63,20 @@ export function Swap({ header }: Props): ReactElement { // Set the initial xDAI to swap useEffect(() => { - if (!walletBalance || userInputSwap) { + if (!balance || userInputSwap) { return } const minimumOptimalValue = DAI.fromDecimalString('1').plus(MINIMUM_XDAI) - if (walletBalance.nativeTokenBalance.gte(minimumOptimalValue)) { + if (balance.dai.gte(minimumOptimalValue)) { // Balance has at least 1 + MINIMUM_XDAI xDai - setDaiToSwap(walletBalance.nativeTokenBalance.minus(DAI.fromDecimalString('1'))) + setDaiToSwap(balance.dai.minus(DAI.fromDecimalString('1'))) } else { // Balance is low, halve the amount - setDaiToSwap(walletBalance.nativeTokenBalance.divide(BigInt(2))) + setDaiToSwap(balance.dai.divide(BigInt(2))) } - }, [walletBalance, userInputSwap]) + }, [balance, userInputSwap]) // Set the xDAI to swap based on user input useEffect(() => { @@ -95,13 +97,13 @@ export function Swap({ header }: Props): ReactElement { // Calculate the amount of tokens after swap useEffect(() => { - if (!walletBalance || !daiToSwap || error) { + if (!balance || !daiToSwap || error) { return } - const daiAfterSwap = walletBalance.nativeTokenBalance.minus(daiToSwap) + const daiAfterSwap = balance.dai.minus(daiToSwap) setDaiAfterSwap(daiAfterSwap) const tokensConverted = daiToSwap.exchangeToBZZ(price) - const bzzAfterSwap = tokensConverted.plus(walletBalance.bzzBalance) + const bzzAfterSwap = tokensConverted.plus(balance.bzz) setBzzAfterSwap(bzzAfterSwap) if (daiAfterSwap.lt(MINIMUM_XDAI)) { @@ -109,9 +111,9 @@ export function Swap({ header }: Props): ReactElement { } else if (bzzAfterSwap.lt(MINIMUM_XBZZ)) { setError(`Must have at least ${MINIMUM_XBZZ.toSignificantDigits(4)} xBZZ after swap!`) } - }, [error, walletBalance, daiToSwap, price]) + }, [error, balance, daiToSwap, price]) - if (!walletBalance || !nodeAddresses || !daiToSwap || !bzzAfterSwap || !daiAfterSwap) { + if (!balance || !nodeAddresses || !daiToSwap || !bzzAfterSwap || !daiAfterSwap) { return } @@ -219,8 +221,8 @@ export function Swap({ header }: Props): ReactElement { - Your current balance is {walletBalance.nativeTokenBalance.toSignificantDigits(4)} xDAI and{' '} - {walletBalance.bzzBalance.toSignificantDigits(4)} xBZZ. + Your current balance is {balance.dai.toSignificantDigits(4)} xDAI and {balance.bzz.toSignificantDigits(4)}{' '} + xBZZ. diff --git a/src/pages/top-up/index.tsx b/src/pages/top-up/index.tsx index 40e4787..e6164c1 100644 --- a/src/pages/top-up/index.tsx +++ b/src/pages/top-up/index.tsx @@ -1,5 +1,5 @@ -import { Box, createStyles, Grid, makeStyles, Typography } from '@material-ui/core' import { BeeModes, BZZ, DAI } from '@ethersphere/bee-js' +import { Box, createStyles, Grid, makeStyles, Typography } from '@material-ui/core' import { useSnackbar } from 'notistack' import { ReactElement, useContext, useState } from 'react' import { useNavigate } from 'react-router' @@ -15,6 +15,7 @@ import { SwarmButton } from '../../components/SwarmButton' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import { Context as BeeContext, CheckState } from '../../providers/Bee' import { Context as SettingsContext } from '../../providers/Settings' +import { Context as BalanceProvider } from '../../providers/WalletBalance' import { ROUTES } from '../../routes' import { restartBeeNode, upgradeToLightNode } from '../../utils/desktop' @@ -39,7 +40,8 @@ export default function TopUp(): ReactElement { const navigate = useNavigate() const styles = useStyles() const { isDesktop, desktopUrl } = useContext(SettingsContext) - const { nodeInfo, status, walletBalance } = useContext(BeeContext) + const { nodeInfo, status } = useContext(BeeContext) + const { balance } = useContext(BalanceProvider) const { rpcProviderUrl } = useContext(SettingsContext) const [loading, setLoading] = useState(false) const { enqueueSnackbar } = useSnackbar() @@ -47,8 +49,8 @@ export default function TopUp(): ReactElement { const canUpgradeToLightNode = isDesktop && nodeInfo?.beeMode === BeeModes.ULTRA_LIGHT && - walletBalance?.nativeTokenBalance.gte(MINIMUM_XDAI) && - walletBalance?.bzzBalance.gte(MINIMUM_XBZZ) + balance?.dai.gte(MINIMUM_XDAI) && + balance?.bzz.gte(MINIMUM_XBZZ) async function restart() { setLoading(true) @@ -65,7 +67,7 @@ export default function TopUp(): ReactElement { if (status.all === CheckState.ERROR) return - if (!walletBalance) { + if (!balance) { return } diff --git a/src/providers/WalletBalance.tsx b/src/providers/WalletBalance.tsx new file mode 100644 index 0000000..92224ce --- /dev/null +++ b/src/providers/WalletBalance.tsx @@ -0,0 +1,88 @@ +import { createContext, ReactChild, ReactElement, useContext, useEffect, useState } from 'react' +import { WalletAddress } from '../utils/wallet' +import { Context as BeeContext } from './Bee' +import { Context as SettingsContext } from './Settings' + +interface ContextInterface { + balance: WalletAddress | null + error: Error | null + isLoading: boolean + lastUpdate: number | null + start: (frequency?: number) => void + stop: () => void + refresh: () => Promise +} + +const initialValues: ContextInterface = { + balance: null, + error: null, + isLoading: false, + lastUpdate: null, + start: () => {}, // eslint-disable-line + stop: () => {}, // eslint-disable-line + refresh: () => Promise.reject(), +} + +export const Context = createContext(initialValues) +export const Consumer = Context.Consumer + +interface Props { + children: ReactChild +} + +export function Provider({ children }: Props): ReactElement { + const { rpcProvider } = useContext(SettingsContext) + const { nodeAddresses } = useContext(BeeContext) + const [balance, setBalance] = useState(initialValues.balance) + const [error, setError] = useState(initialValues.error) + const [isLoading, setIsLoading] = useState(initialValues.isLoading) + const [lastUpdate, setLastUpdate] = useState(initialValues.lastUpdate) + const [frequency, setFrequency] = useState(null) + + useEffect(() => { + if (nodeAddresses?.ethereum && rpcProvider) { + WalletAddress.make(nodeAddresses.ethereum.toHex(), rpcProvider).then(setBalance) + } else { + setBalance(null) + } + }, [nodeAddresses, rpcProvider]) + + const refresh = async () => { + // Don't want to refresh when already refreshing + if (isLoading) return + + if (!balance) return + + try { + setIsLoading(true) + + setBalance(await balance.refresh()) + setLastUpdate(Date.now()) + } catch (e) { + setError(e as Error) + } finally { + setIsLoading(false) + } + } + + const start = (freq = 30000) => setFrequency(freq) + const stop = () => setFrequency(null) + + // Start the update loop + useEffect(() => { + refresh() + + // Start autorefresh only if the frequency is set + if (frequency) { + const interval = setInterval(refresh, frequency) + + return () => clearInterval(interval) + } + }, [frequency]) // eslint-disable-line react-hooks/exhaustive-deps + + return ( + + {children} + + ) +} diff --git a/src/utils/wallet.ts b/src/utils/wallet.ts index 4d8fc8c..fd7ecc4 100644 --- a/src/utils/wallet.ts +++ b/src/utils/wallet.ts @@ -2,6 +2,29 @@ import { BZZ, DAI, EthAddress } from '@ethersphere/bee-js' import { providers, Wallet } from 'ethers' import { estimateNativeTransferTransactionCost, Rpc } from './rpc' +export class WalletAddress { + private constructor( + public address: string, + public bzz: BZZ, + public dai: DAI, + public provider: providers.JsonRpcProvider, + ) {} + + static async make(address: string, provider: providers.JsonRpcProvider): Promise { + const bzz = await Rpc._eth_getBalanceERC20(address, provider) + const dai = await Rpc._eth_getBalance(address, provider) + + return new WalletAddress(address, bzz, dai, provider) + } + + public async refresh(): Promise { + this.bzz = await Rpc._eth_getBalanceERC20(this.address, this.provider) + this.dai = await Rpc._eth_getBalance(this.address, this.provider) + + return this + } +} + export class ResolvedWallet { public address: string public privateKey: string