import { BatchId, BeeRequestOptions, BZZ, capacityBreakpoints, Duration, PostageBatch, RedundancyLevel, Size, Utils, } from '@ethersphere/bee-js' import { Warning } from '@mui/icons-material' import { DriveInfo } from '@solarpunkltd/file-manager-lib' import { ReactElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import { createPortal } from 'react-dom' import CalendarIcon from 'remixicon-react/CalendarLineIcon' import DatabaseIcon from 'remixicon-react/Database2LineIcon' import ExternalLinkIcon from 'remixicon-react/ExternalLinkLineIcon' import DriveIcon from 'remixicon-react/HardDrive2LineIcon' import WalletIcon from 'remixicon-react/Wallet3LineIcon' import { Context as BeeContext } from '../../../../providers/Bee' import { Context as FMContext } from '../../../../providers/FileManager' import { Context as SettingsContext } from '../../../../providers/Settings' import { getHumanReadableFileSize } from '../../../../utils/file' import { FILE_MANAGER_EVENTS, POLLING_TIMEOUT_MS } from '../../constants/common' import { desiredLifetimeOptions } from '../../constants/stamps' import { useStampPolling } from '../../hooks/useStampPolling' import { validateStampStillExists } from '../../utils/bee' import { fromBytesConversion, getExpiryDateByLifetime, truncateNameMiddle } from '../../utils/common' import { Button } from '../Button/Button' import { CustomDropdown } from '../CustomDropdown/CustomDropdown' import './UpgradeDriveModal.scss' import '../../styles/global.scss' interface UpgradeDriveModalProps { stamp: PostageBatch drive: DriveInfo onCancelClick: () => void containerColor?: string setErrorMessage?: (error: string) => void } const defaultErasureCodeLevel = RedundancyLevel.OFF const encryption_off = 'ENCRYPTION_OFF' export function UpgradeDriveModal({ stamp, onCancelClick, containerColor, drive, setErrorMessage, }: UpgradeDriveModalProps): ReactElement { const { nodeAddresses, walletBalance } = useContext(BeeContext) const { beeApi } = useContext(SettingsContext) const { refreshStamp, setShowError } = useContext(FMContext) const [isBalanceSufficient, setIsBalanceSufficient] = useState(true) const [capacity, setCapacity] = useState(stamp.size) const [capacityExtensionCost, setCapacityExtensionCost] = useState('') const [capacityIndex, setCapacityIndex] = useState(0) const [durationExtensionCost, setDurationExtensionCost] = useState('') const [lifetimeIndex, setLifetimeIndex] = useState(0) const validityEndDate = useMemo( () => getExpiryDateByLifetime(lifetimeIndex, stamp.duration.toEndDate()), [lifetimeIndex, stamp.duration], ) const [sizeMarks, setSizeMarks] = useState<{ value: number; label: string }[]>([]) const [extensionCost, setExtensionCost] = useState('0') const [isSubmitting, setIsSubmitting] = useState(false) const modalRoot = document.querySelector('.fm-main') || document.body const isMountedRef = useRef(true) const { startPolling } = useStampPolling({ refreshStamp, onStampUpdated: (updatedStamp: PostageBatch) => { window.dispatchEvent( new CustomEvent(FILE_MANAGER_EVENTS.DRIVE_UPGRADE_END, { detail: { driveId: drive.id.toString(), success: true, updatedStamp, }, }), ) }, onTimeout: (finalStamp: PostageBatch | null) => { window.dispatchEvent( new CustomEvent(FILE_MANAGER_EVENTS.DRIVE_UPGRADE_TIMEOUT, { detail: { driveId: drive.id.toString(), finalStamp: finalStamp || null, }, }), ) }, onPollingStateChange: () => { // no-op }, timeout: POLLING_TIMEOUT_MS, }) const handleCapacityChange = (value: number, index: number) => { setCapacity(value === -1 ? stamp.size : Size.fromBytes(value)) setCapacityIndex(index) } const handleCostCalculation = useCallback( async ( batchId: BatchId, capacity: Size, duration: Duration, options: BeeRequestOptions | undefined, encryption: boolean, erasureCodeLevel: RedundancyLevel, isCapacityExtensionSet: boolean, ) => { let cost: BZZ | undefined try { cost = await beeApi?.getExtensionCost(batchId, capacity, duration, options, encryption, erasureCodeLevel) } catch { setErrorMessage?.('Failed to calculate extension cost') setShowError(true) return } const costText = cost ? cost.toSignificantDigits(2) : '0' if (!isMountedRef.current) return if ((walletBalance && cost && cost.gte(walletBalance.bzzBalance)) || !walletBalance) { setIsBalanceSufficient(false) } else { setIsBalanceSufficient(true) } if (isCapacityExtensionSet) { setCapacityExtensionCost('') setDurationExtensionCost('') } else { setCapacityExtensionCost('0') setDurationExtensionCost(costText) } setExtensionCost(costText) }, [beeApi, walletBalance, isMountedRef, setErrorMessage, setShowError], ) useEffect(() => { isMountedRef.current = true return () => { isMountedRef.current = false } }, []) useEffect(() => { const fetchSizes = () => { const sizes = Array.from(Utils.getStampEffectiveBytesBreakpoints(false, defaultErasureCodeLevel).values()) const capacityValues = capacityBreakpoints[encryption_off][defaultErasureCodeLevel] const fromIndex = capacityValues.findIndex(item => item.batchDepth === stamp.depth) const newSizes = sizes.slice(fromIndex + 1) const updatedSizes = [ { value: -1, label: 'No additional storage (0 GB)' }, ...newSizes.map(size => ({ value: size, label: getHumanReadableFileSize(size - stamp.size.toBytes()), })), ] setSizeMarks(updatedSizes) } fetchSizes() }, [stamp.depth, stamp.size]) useEffect(() => { const fetchExtensionCost = () => { const isCapacitySet = capacityIndex > 0 const extendDuration = Duration.fromEndDate(validityEndDate, stamp.duration.toEndDate()) handleCostCalculation( stamp.batchID, capacity, extendDuration, undefined, false, defaultErasureCodeLevel, isCapacitySet, ) } fetchExtensionCost() }, [capacity, validityEndDate, capacityIndex, handleCostCalculation, stamp.batchID, stamp.duration]) const batchIdStr = stamp.batchID.toString() const shortBatchId = batchIdStr.length > 12 ? `${batchIdStr.slice(0, 4)}...${batchIdStr.slice(-4)}` : batchIdStr return createPortal(