Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { PostageBatch } from '@ethersphere/bee-js'
import { Bee, PostageBatch } from '@ethersphere/bee-js'
import { DriveInfo, FileInfo } from '@solarpunkltd/file-manager-lib'
import { ReactElement, useEffect, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import AlertIcon from 'remixicon-react/AlertLineIcon'

import { validateStampStillExists } from '../../utils/bee'
import { getDaysLeft } from '../../utils/common'
import { Button } from '../Button/Button'
import { UpgradeDriveModal } from '../UpgradeDriveModal/UpgradeDriveModal'
Expand All @@ -16,19 +17,23 @@ import '../../styles/global.scss'
const EXPIRING_ITEMS_PAGE_SIZE = 3

interface ExpiringNotificationModalProps {
bee: Bee
stamps: PostageBatch[]
drives: DriveInfo[]
files: FileInfo[]
onCancelClick: () => void
setErrorMessage?: (error: string) => void
setShowError: (show: boolean) => void
}

export function ExpiringNotificationModal({
bee,
stamps,
drives,
files,
onCancelClick,
setErrorMessage,
setShowError,
}: ExpiringNotificationModalProps): ReactElement {
const [showUpgradeDriveModal, setShowUpgradeDriveModal] = useState(false)
const [actualStamp, setActualStamp] = useState<PostageBatch | undefined>(undefined)
Expand Down Expand Up @@ -75,7 +80,18 @@ export function ExpiringNotificationModal({
files={files}
currentPage={currentPage}
index={index}
onUpgradeClick={(stamp, drive) => {
onUpgradeClick={async (stamp, drive) => {
const isStampValid = await validateStampStillExists(bee, stamp.batchID)

if (!isStampValid) {
setErrorMessage?.(
`Drive ${drive.name} has expired. Please clear the browser cache and reload the page.`,
)
setShowError(true)

return
}

setActualStamp(stamp)
setActualDrive(drive)
setShowUpgradeDriveModal(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import React, {
useRef,
useState,
} from 'react'
import { createPortal } from 'react-dom'

import { useSearch } from '../../../../pages/filemanager/SearchContext'
import { useView } from '../../../../pages/filemanager/ViewContext'
Expand Down Expand Up @@ -87,7 +88,9 @@ function ErrorModalBlock({
return null
}

return <ErrorModal label={label} onClick={onOk} />
const modalRoot = document.querySelector('.fm-main') || document.body

return createPortal(<ErrorModal label={label} onClick={onOk} />, modalRoot)
}

const extractFilesFromClipboardEvent = (e: React.ClipboardEvent): File[] => {
Expand Down Expand Up @@ -431,8 +434,10 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
if (rafIdRef.current) {
cancelAnimationFrame(rafIdRef.current)
}

setShowError(false)
}
}, [])
}, [setShowError])

useEffect(() => {
let title = currentDrive?.name || ''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function NotificationBar({ setErrorMessage }: NotificationBarProps): Reac
const [stampsToExpire, setStampsToExpire] = useState<PostageBatch[]>([])
const [drivesToExpire, setDrivesToExpire] = useState<DriveInfo[]>([])
const { beeApi } = useContext(SettingsContext)
const { drives, files, adminDrive } = useContext(FMContext)
const { drives, files, adminDrive, setShowError } = useContext(FMContext)

const showExpiration = stampsToExpire.length > 0

Expand Down Expand Up @@ -109,15 +109,17 @@ export function NotificationBar({ setErrorMessage }: NotificationBarProps): Reac
<div className="fm-notification-bar fm-red-font" onClick={() => setShowExpiringModal(true)}>
{stampsToExpire.length} drive{stampsToExpire.length > 1 ? 's' : ''} expiring soon <UpIcon size="16px" />
</div>
{showExpiringModal && (
{showExpiringModal && beeApi && (
<ExpiringNotificationModal
bee={beeApi}
stamps={stampsToExpire}
drives={drivesToExpire}
files={files}
onCancelClick={() => {
setShowExpiringModal(false)
}}
setErrorMessage={setErrorMessage}
setShowError={setShowError}
/>
)}
</>
Expand Down
23 changes: 22 additions & 1 deletion src/pages/filemanager/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BeeModes } from '@ethersphere/bee-js'
import { DriveInfo, FileManagerBase } from '@solarpunkltd/file-manager-lib'
import { ReactElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'

Expand Down Expand Up @@ -44,6 +45,18 @@ function InitializationErrorBlock({ onOk }: { onOk: () => void }) {
)
}

function UltraLightNodeErrorBlock() {
return (
<div className="fm-main">
<div className="fm-loading">
<div className="fm-loading-title">
File Manager is not available with an Ultra-light node. Please upgrade to a Light node to continue.
</div>
</div>
</div>
)
}

function ResetModalBlock({ cacheHelpUrl, onConfirm }: { cacheHelpUrl: string; onConfirm: () => void }) {
return (
<div className="fm-main">
Expand Down Expand Up @@ -169,6 +182,7 @@ function FileManagerMainContent(props: {

enum PageState {
Connecting = 'connecting', // still warming up — show nothing / loader
UltraLightNode = 'ultra-light-node', // ultra-light node — file manager not available
NoPrivateKey = 'no-pk', // private key not set
Loading = 'loading', // bee ready, pk present, FM init in progress
Reset = 'reset', // STATE_INVALID emitted and user has not yet acknowledged
Expand All @@ -189,7 +203,7 @@ export function FileManagerPage(): ReactElement {
const [connectionErrorDismissed, setConnectionErrorDismissed] = useState<boolean>(false)
const [cacheHelpUrl, setCacheHelpUrl] = useState<string>(cacheClearUrls[BrowserPlatform.Chrome])

const { status, chainState } = useContext(BeeContext)
const { status, chainState, nodeInfo } = useContext(BeeContext)
const { fm, initDone, shallReset, adminDrive, initializationError, notifyPkSaved } = useContext(FMContext)

useEffect(() => {
Expand Down Expand Up @@ -228,6 +242,8 @@ export function FileManagerPage(): ReactElement {

if (!isBeeReady && !initDone) return PageState.Connecting

if (nodeInfo?.beeMode === BeeModes.ULTRA_LIGHT) return PageState.UltraLightNode

if (!hasPk) return PageState.NoPrivateKey

if (!initDone) return PageState.Loading
Expand Down Expand Up @@ -259,6 +275,7 @@ export function FileManagerPage(): ReactElement {
adminDrive,
isCreationInProgress,
chainState,
nodeInfo?.beeMode,
])

const handlePrivateKeySaved = useCallback(() => {
Expand All @@ -274,6 +291,10 @@ export function FileManagerPage(): ReactElement {
const loading = !fm?.adminStamp || !adminDrive
const isFormbricksActive = Boolean(fm && fm.adminStamp && adminDrive && !loading)

if (pageState === PageState.UltraLightNode) {
return <UltraLightNodeErrorBlock />
}

if (pageState === PageState.Connecting || pageState === PageState.Loading) {
return <LoadingBlock />
}
Expand Down
1 change: 0 additions & 1 deletion src/pages/stamps/PostageStampStandardCreation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ export function PostageStampStandardCreation({ onFinished }: Props): ReactElemen
<Grid container justifyContent="space-between">
<Typography>Corresponding TTL (Time to live)</Typography>
<Typography>{amountInput ? getTtl(amountInput) : '-'}</Typography>
<Typography>{amountInput ? getTtl(amountInput) : '-'}</Typography>
</Grid>
</Box>
<Box display="flex" justifyContent={'right'} mt={0.5}>
Expand Down
4 changes: 2 additions & 2 deletions src/utils/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export async function sendNativeTransaction(
const feedData = await signer.provider.getFeeData()
const gasPrice = externalGasPrice ?? DAI.fromWei(feedData.gasPrice?.toString() || '0')
const transaction = await signer.sendTransaction({
to: to.toHex(),
to: to.toChecksum(),
value: BigInt(value.toWeiString()),
gasPrice: BigInt(gasPrice.toWeiString()),
gasLimit: BigInt(21000),
Expand Down Expand Up @@ -117,7 +117,7 @@ export async function sendBzzTransaction(
const feeData = await signer.provider.getFeeData()
const gasPrice = feeData.gasPrice || BigInt(0)
const bzz = new Contract(BZZ_TOKEN_ADDRESS, bzzABI, signer)
const transaction = await bzz.transfer(to.toChecksum(), value, { gasPrice })
const transaction = await bzz.transfer(to.toChecksum(), value.toPLURBigInt(), { gasPrice })
const receipt = await transaction.wait(1)

if (!receipt) {
Expand Down
56 changes: 52 additions & 4 deletions tests/unit/rpc.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BZZ } from '@ethersphere/bee-js'
import { BZZ, DAI } from '@ethersphere/bee-js'

import { sendBzzTransaction } from '../../src/utils/rpc'
import { sendBzzTransaction, sendNativeTransaction } from '../../src/utils/rpc'

interface MockProvider {
getFeeData: jest.Mock
Expand All @@ -11,12 +11,14 @@ const mockWait = jest.fn()
const mockTransfer = jest.fn()
const mockGetFeeData = jest.fn()
const mockGetNetwork = jest.fn()
const mockSendTransaction = jest.fn()
const mockProvider: MockProvider = {
getFeeData: mockGetFeeData,
getNetwork: mockGetNetwork,
}

const value = BZZ.fromDecimalString('1')
const bzzValue = BZZ.fromDecimalString('1')
const daiValue = DAI.fromDecimalString('1')
const privateKey = 'FFFF000000000000000000000000000000000000000000000000000000000000'
const jsonRpcProvider = 'http://mock-json-rpc-provider'

Expand All @@ -39,6 +41,7 @@ jest.mock('ethers', () => {

class Wallet {
provider: MockProvider
sendTransaction = mockSendTransaction

constructor(_privateKey: string, provider: MockProvider) {
this.provider = provider
Expand All @@ -64,8 +67,53 @@ describe('sendBzzTransaction', () => {
})

it.each(addresses)('sendBzzTransaction to address: %s', async (address: string) => {
await sendBzzTransaction(privateKey, address, value, jsonRpcProvider)
await sendBzzTransaction(privateKey, address, bzzValue, jsonRpcProvider)
const to = mockTransfer.mock.calls[0][0] as string
expect(to.startsWith('0x')).toBe(true)
})

it('passes BZZ value as bigint (not BZZ object)', async () => {
await sendBzzTransaction(privateKey, '0x52908400098527886e0f7030069857d2e4169ee7', bzzValue, jsonRpcProvider)
const transferredValue = mockTransfer.mock.calls[0][1]
expect(typeof transferredValue).toBe('bigint')
expect(transferredValue).toBe(bzzValue.toPLURBigInt())
})
})

describe('sendNativeTransaction', () => {
const addresses = ['52908400098527886e0f7030069857d2e4169ee7', '0x52908400098527886e0f7030069857d2e4169ee7']

beforeEach(() => {
jest.clearAllMocks()
mockWait.mockResolvedValue({ status: 1 })
mockSendTransaction.mockResolvedValue({ wait: mockWait })
mockGetFeeData.mockResolvedValue({ gasPrice: BigInt(1) })
mockGetNetwork.mockResolvedValue({ chainId: BigInt(100) })
})

it.each(addresses)('sendNativeTransaction to address: %s passes 0x-prefixed address', async (address: string) => {
await sendNativeTransaction(privateKey, address, daiValue, jsonRpcProvider)
const tx = mockSendTransaction.mock.calls[0][0] as { to: string }
expect(tx.to.startsWith('0x')).toBe(true)
})

it('passes DAI value as bigint', async () => {
await sendNativeTransaction(privateKey, '0x52908400098527886e0f7030069857d2e4169ee7', daiValue, jsonRpcProvider)
const tx = mockSendTransaction.mock.calls[0][0] as { value: bigint }
expect(typeof tx.value).toBe('bigint')
expect(tx.value).toBe(BigInt(daiValue.toWeiString()))
})

it('uses externalGasPrice when provided', async () => {
const externalGasPrice = DAI.fromWei('9999')
await sendNativeTransaction(
privateKey,
'0x52908400098527886e0f7030069857d2e4169ee7',
daiValue,
jsonRpcProvider,
externalGasPrice,
)
const tx = mockSendTransaction.mock.calls[0][0] as { gasPrice: bigint }
expect(tx.gasPrice).toBe(BigInt(externalGasPrice.toWeiString()))
})
})
Loading