import { BatchId, Bee, BZZ, Duration, PostageBatch, RedundancyLevel, Size } from '@ethersphere/bee-js' import { DriveInfo, estimateDriveListMetadataSize, estimateFileInfoMetadataSize, FileInfo, FileManagerBase, } from '@solarpunkltd/file-manager-lib' import React from 'react' import { getHumanReadableFileSize } from '../../../utils/file' import { ActionTag } from '../constants/transfers' export const getUsableStamps = async (bee: Bee | null): Promise => { if (!bee) { return [] } try { return (await bee.getPostageBatches()) .filter(s => s.usable) .sort((a, b) => (a.label || '').localeCompare(b.label || '')) } catch { return [] } } export const validateStampStillExists = async (bee: Bee, batchId: BatchId): Promise => { try { const stamp = await bee.getPostageBatch(batchId.toString()) return stamp.usable } catch (error) { // eslint-disable-next-line no-console console.warn(`Failed to validate stamp ${batchId.toString().slice(0, 8)}...:`, error) return false } } export const fmGetStorageCost = async ( capacity: number, validityEndDate: Date, encryption: boolean, erasureCodeLevel: RedundancyLevel, beeApi: Bee | null, ): Promise => { try { if (Size.fromBytes(capacity).toGigabytes() >= 0 && validityEndDate.getTime() >= new Date().getTime()) { const cost = await beeApi?.getStorageCost( Size.fromBytes(capacity), Duration.fromEndDate(validityEndDate), undefined, encryption, erasureCodeLevel, ) return cost } return undefined } catch { return undefined } } export const fmFetchCost = async ( capacity: number, validityEndDate: Date, encryption: boolean, erasureCodeLevel: RedundancyLevel, beeApi: Bee | null, setCost: (cost: BZZ) => void, currentFetch: React.MutableRefObject | null>, onError?: (error: unknown) => void, ) => { if (currentFetch.current) { await currentFetch.current } let isCurrentFetch = true const fetchPromise = (async () => { try { const cost = await fmGetStorageCost(capacity, validityEndDate, encryption, erasureCodeLevel, beeApi) if (isCurrentFetch) { if (cost) { setCost(cost) } else { setCost(BZZ.fromDecimalString('0')) onError?.(new Error('Storage cost unavailable - node may be syncing')) } } } catch (error) { if (isCurrentFetch) { setCost(BZZ.fromDecimalString('0')) onError?.(error) } } })() currentFetch.current = fetchPromise await fetchPromise isCurrentFetch = false currentFetch.current = null } export interface CreateDriveOptions { beeApi: Bee | null fm: FileManagerBase | null size: Size duration: Duration label: string encryption: boolean redundancyLevel: RedundancyLevel adminRedundancy: RedundancyLevel isAdmin: boolean resetState: boolean existingBatch: PostageBatch | null onSuccess?: () => void onError?: (error: unknown) => void } export const handleCreateDrive = async (options: CreateDriveOptions): Promise => { const { beeApi, fm, size, duration, label, encryption, redundancyLevel, adminRedundancy, isAdmin, resetState, existingBatch, onSuccess, onError, } = { ...options } if (!beeApi || !fm) { // eslint-disable-next-line no-console console.error('Error creating drive: Bee API or FM is invalid!') onError?.('Error creating drive: Bee API or FM is invalid!') return } try { let batchId: BatchId if (!existingBatch) { if (!isAdmin) { if (!fm.adminStamp) { // eslint-disable-next-line no-console console.error('Error creating drive: admin stamp is not available') throw new Error('Error creating drive: admin stamp is not available') } verifyDriveSpace({ fm, redundancyLevel, stamp: fm.adminStamp, adminRedundancy, cb: err => { throw new Error(err) }, }) } batchId = await beeApi.buyStorage(size, duration, { label }, undefined, encryption, redundancyLevel) } else { const isValid = await validateStampStillExists(beeApi, existingBatch.batchID) if (!isValid) { throw new Error( 'The stamp is no longer valid or has been deleted. Please select a different stamp from the list.', ) } verifyDriveSpace({ fm, redundancyLevel, stamp: existingBatch, adminRedundancy, cb: err => { throw new Error(err) }, }) batchId = existingBatch.batchID } await fm.createDrive(batchId, label, isAdmin, redundancyLevel, resetState) onSuccess?.() } catch (e) { // eslint-disable-next-line no-console console.error('Error creating drive:', e instanceof Error ? e.message : String(e)) onError?.(e) } } export interface DestroyDriveOptions { beeApi?: Bee | null fm: FileManagerBase | null drive: DriveInfo adminDrive: DriveInfo | null isDestroy: boolean onSuccess?: () => void onError?: (error: unknown) => void } export const handleDestroyAndForgetDrive = async (options: DestroyDriveOptions): Promise => { const { beeApi, fm, adminDrive, drive, isDestroy, onSuccess, onError } = { ...options } if (!beeApi) { onError?.('Bee API is invalid!') return } if (!fm || !fm.adminStamp || !adminDrive) { onError?.('FM is invalid!') return } try { verifyDriveSpace({ fm, driveId: drive.id.toString(), redundancyLevel: drive.redundancyLevel, stamp: fm.adminStamp, isRemove: true, adminRedundancy: adminDrive.redundancyLevel, cb: err => { throw new Error(err) }, }) if (!isDestroy) { await fm.forgetDrive(drive) onSuccess?.() return } const driveStamp = (await getUsableStamps(beeApi)).find(s => s.batchID.toString() === drive.batchId.toString()) const ttlDays = driveStamp?.duration.toDays() ?? 0 if (!driveStamp || ttlDays <= 2) { // eslint-disable-next-line no-console console.warn(`Stamp not found or TTL ${ttlDays} <= 2 days, skipping drive destruction: forgetting the drive.`) await fm.forgetDrive(drive) onSuccess?.() return } await fm.destroyDrive(drive, driveStamp) onSuccess?.() } catch (e) { onError?.(e) } } export interface StampCapacityMetrics { capacityPct: number usedSize: string stampSize: string usedBytes: number stampSizeBytes: number remainingBytes: number } export const calculateStampCapacityMetrics = ( stamp: PostageBatch, files: FileInfo[], redundancyLevel?: RedundancyLevel, useReportedOnly?: boolean, ): StampCapacityMetrics => { let stampSizeBytes = 0 let remainingReportedBytes = 0 if (redundancyLevel !== undefined) { stampSizeBytes = stamp.calculateSize(false, redundancyLevel).toBytes() remainingReportedBytes = stamp.calculateRemainingSize(false, redundancyLevel).toBytes() } else { stampSizeBytes = stamp.size.toBytes() remainingReportedBytes = stamp.remainingSize.toBytes() } const usedBytesReported = stampSizeBytes - remainingReportedBytes const pctReportedStampUsage = stamp.usage * 100 let usedSizeMaxBytes = usedBytesReported let pctFromDriveUsage = pctReportedStampUsage let remainingBytes = remainingReportedBytes if (!useReportedOnly) { const usedBytesFromFiles = files .map(f => { let rawSize = 0 const lifecycle = (f.customMetadata?.lifecycle || '').toString().toLowerCase() const isLifecycleOperation = lifecycle === ActionTag.Trashed || lifecycle === ActionTag.Recovered || lifecycle === ActionTag.Restored if ( (f.customMetadata?.size && !isLifecycleOperation) || (f.customMetadata?.size && !f.customMetadata?.accumulatedSize) ) { rawSize = Number(f.customMetadata.size) } const accumulatedSize = Number(f.customMetadata?.accumulatedSize || rawSize || 0) return accumulatedSize }) .reduce((acc, current) => acc + current, 0) const remainingBytesFromFiles = stampSizeBytes - usedBytesFromFiles > 0 ? stampSizeBytes - usedBytesFromFiles : 0 remainingBytes = Math.min(remainingReportedBytes, remainingBytesFromFiles) usedSizeMaxBytes = Math.max(usedBytesFromFiles, usedBytesReported) pctFromDriveUsage = stampSizeBytes > 0 ? (usedSizeMaxBytes / stampSizeBytes) * 100 : 0 } const usedSizeMax = getHumanReadableFileSize(usedSizeMaxBytes) const capacityPct = Math.max(pctFromDriveUsage, pctReportedStampUsage) const stampSize = getHumanReadableFileSize(stampSizeBytes) return { capacityPct, usedSize: usedSizeMax, stampSize, usedBytes: usedSizeMaxBytes, stampSizeBytes, remainingBytes, } } export interface DriveSpaceOptions { fm: FileManagerBase driveId?: string redundancyLevel: RedundancyLevel stamp: PostageBatch adminRedundancy?: RedundancyLevel useInfoSize?: boolean isRemove?: boolean fileSize?: number fileCount?: number cb?: (msg: string) => void } export const verifyDriveSpace = ( options: DriveSpaceOptions, ): { remainingBytes: number; totalSizeBytes: number; ok: boolean } => { const { fm, driveId, redundancyLevel, stamp, adminRedundancy, useInfoSize, isRemove, fileSize, fileCount, cb } = { ...options, } const drives = [...fm.driveList] let filesPerDrives: FileInfo[] = [] // new drivelist state size calc. if (isRemove) { const driveIx = drives.findIndex(d => d.id.toString() === driveId?.toString()) if (driveIx === -1) { cb?.(`Admin drive not found during stamp verification`) return { remainingBytes: 0, totalSizeBytes: 0, ok: false } } drives.splice(driveIx, 1) filesPerDrives = fm.fileInfoList.filter(fi => fi.driveId !== driveId) } else { filesPerDrives = driveId ? fm.fileInfoList.filter(fi => fi.driveId === driveId) : [] } // admin stamp capacity calcl., needed for forget, destroy, create if (adminRedundancy !== undefined && fm.adminStamp) { // upper limit estimate on the drivelist metadata state size based on the number of drives and files const estimatedDlSizeBytes = estimateDriveListMetadataSize(drives) * drives.length const { remainingBytes: remainingAdminBytes } = calculateStampCapacityMetrics(fm.adminStamp, [], adminRedundancy) const ok = remainingAdminBytes >= estimatedDlSizeBytes if (!ok) { cb?.( `Insufficient admin drive capacity. Required: ~${getHumanReadableFileSize( estimatedDlSizeBytes, )} bytes, Available: ${getHumanReadableFileSize( remainingAdminBytes, )} bytes. Please top up the admin drive/stamp.`, ) return { remainingBytes: remainingAdminBytes, totalSizeBytes: estimatedDlSizeBytes, ok } } } // other fileinfo metadata size calc. const estimatedFiSize = estimateFileInfoMetadataSize() const count = fileCount ?? 1 const estimateReqSizeBytes = Number(Boolean(useInfoSize)) * estimatedFiSize * count + (fileSize ? fileSize : 0) const { remainingBytes } = calculateStampCapacityMetrics(stamp, filesPerDrives, redundancyLevel) const ok = remainingBytes >= estimateReqSizeBytes if (!ok) { cb?.( `Insufficient capacity. Required: ~${getHumanReadableFileSize( estimateReqSizeBytes, )} bytes, Available: ${getHumanReadableFileSize(remainingBytes)} bytes. Please top up the drive/stamp.`, ) } return { remainingBytes, totalSizeBytes: estimateReqSizeBytes, ok } }