diff --git a/assets/config/dev.json b/assets/config/dev.json index c3e711ae36..f186abb023 100644 --- a/assets/config/dev.json +++ b/assets/config/dev.json @@ -2,7 +2,7 @@ "defaultArweaveGatewayUrl": "https://arweave.net", "useTurboUpload": true, "useTurboPayment": true, - "defaultTurboUploadUrl": "https://upload.ardrive.io", + "defaultTurboUploadUrl": "https://upload.ardrive.dev", "defaultTurboPaymentUrl": "https://payment.ardrive.dev", "allowedDataItemSizeForTurbo": 500000, "enableQuickSyncAuthoring": true, diff --git a/assets/config/prod.json b/assets/config/prod.json index 5f5cf65e46..904caa6cdb 100644 --- a/assets/config/prod.json +++ b/assets/config/prod.json @@ -11,5 +11,5 @@ "enableAudioPreview": true, "stripePublishableKey": "pk_live_51JUAtwC8apPOWkDLMQqNF9sPpfneNSPnwX8YZ8y1FNDl6v94hZIwzgFSYl27bWE4Oos8CLquunUswKrKcaDhDO6m002Yj9AeKj", "enablePins": true, - "useNewUploader": false + "useNewUploader": true } diff --git a/lib/blocs/create_manifest/create_manifest_cubit.dart b/lib/blocs/create_manifest/create_manifest_cubit.dart index 11aa3a192e..199c5aff87 100644 --- a/lib/blocs/create_manifest/create_manifest_cubit.dart +++ b/lib/blocs/create_manifest/create_manifest_cubit.dart @@ -3,17 +3,18 @@ import 'dart:async'; import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/entities/entities.dart'; import 'package:ardrive/entities/manifest_data.dart'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/turbo/services/upload_service.dart'; import 'package:ardrive/utils/logger/logger.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; import 'package:arweave/utils.dart'; import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pst/pst.dart'; import 'package:uuid/uuid.dart'; import '../../core/upload/cost_calculator.dart'; diff --git a/lib/blocs/create_snapshot/create_snapshot_cubit.dart b/lib/blocs/create_snapshot/create_snapshot_cubit.dart index 2c6e6b41fe..c9457c438a 100644 --- a/lib/blocs/create_snapshot/create_snapshot_cubit.dart +++ b/lib/blocs/create_snapshot/create_snapshot_cubit.dart @@ -8,7 +8,6 @@ import 'package:ardrive/blocs/profile/profile_cubit.dart'; import 'package:ardrive/blocs/upload/upload_cubit.dart'; import 'package:ardrive/core/upload/cost_calculator.dart'; import 'package:ardrive/entities/snapshot_entity.dart'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/models/daos/daos.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/turbo/services/payment_service.dart'; @@ -25,6 +24,7 @@ import 'package:arweave/arweave.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pst/pst.dart'; import 'package:stash_shared_preferences/stash_shared_preferences.dart'; import 'package:uuid/uuid.dart'; @@ -421,7 +421,7 @@ class CreateSnapshotCubit extends Cubit { _turboBalance = turboBalance; _hasNoTurboBalance = turboBalance == BigInt.zero; - _turboCredits = convertCreditsToLiteralString(turboBalance); + _turboCredits = convertWinstonToLiteralString(turboBalance); _sufficentCreditsBalance = _costEstimateTurbo.totalCost <= _turboBalance; _computeIsTurboEnabled(); _computeIsButtonEnabled(); @@ -464,8 +464,8 @@ class CreateSnapshotCubit extends Cubit { _turboBalance = turboBalance; _hasNoTurboBalance = turboBalance == BigInt.zero; - _turboCredits = convertCreditsToLiteralString(turboBalance); - _arBalance = convertCreditsToLiteralString(auth.currentUser.walletBalance); + _turboCredits = convertWinstonToLiteralString(turboBalance); + _arBalance = convertWinstonToLiteralString(auth.currentUser.walletBalance); } void _computeIsTurboEnabled() async { diff --git a/lib/blocs/drive_attach/drive_attach_cubit.dart b/lib/blocs/drive_attach/drive_attach_cubit.dart index acb431a16d..3bac101858 100644 --- a/lib/blocs/drive_attach/drive_attach_cubit.dart +++ b/lib/blocs/drive_attach/drive_attach_cubit.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'package:ardrive/blocs/blocs.dart'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/utils/logger/logger.dart'; diff --git a/lib/blocs/drive_detail/drive_detail_cubit.dart b/lib/blocs/drive_detail/drive_detail_cubit.dart index 1367d7ab3b..263402d45b 100644 --- a/lib/blocs/drive_detail/drive_detail_cubit.dart +++ b/lib/blocs/drive_detail/drive_detail_cubit.dart @@ -3,13 +3,13 @@ import 'dart:async'; import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/entities/constants.dart'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/pages.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/utils/logger/logger.dart'; import 'package:ardrive/utils/open_url.dart'; import 'package:ardrive/utils/user_utils.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:drift/drift.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/blocs/drives/drives_cubit.dart b/lib/blocs/drives/drives_cubit.dart index 7c2e5375f6..c8931c8b43 100644 --- a/lib/blocs/drives/drives_cubit.dart +++ b/lib/blocs/drives/drives_cubit.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/blocs/blocs.dart'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/utils/user_utils.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:drift/drift.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/blocs/file_download/file_download_cubit.dart b/lib/blocs/file_download/file_download_cubit.dart index 5b6810a37b..9d8a8b1c13 100644 --- a/lib/blocs/file_download/file_download_cubit.dart +++ b/lib/blocs/file_download/file_download_cubit.dart @@ -5,9 +5,10 @@ import 'package:ardrive/core/arfs/entities/arfs_entities.dart'; import 'package:ardrive/core/arfs/repository/arfs_repository.dart'; import 'package:ardrive/core/crypto/crypto.dart'; import 'package:ardrive/download/ardrive_downloader.dart'; +import 'package:ardrive/download/limits.dart'; +import 'package:ardrive/entities/constants.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; -import 'package:ardrive/utils/data_size.dart'; import 'package:ardrive/utils/logger/logger.dart'; import 'package:ardrive_http/ardrive_http.dart'; import 'package:ardrive_io/ardrive_io.dart' as io; diff --git a/lib/blocs/file_download/file_download_state.dart b/lib/blocs/file_download/file_download_state.dart index cb0da652e1..9299ef2288 100644 --- a/lib/blocs/file_download/file_download_state.dart +++ b/lib/blocs/file_download/file_download_state.dart @@ -81,6 +81,7 @@ class FileDownloadAborted extends FileDownloadState {} enum FileDownloadFailureReason { unknownError, fileAboveLimit, + browserDoesNotSupportLargeDownloads, networkConnectionError, fileNotFound } diff --git a/lib/blocs/file_download/personal_file_download_cubit.dart b/lib/blocs/file_download/personal_file_download_cubit.dart index 64de257da5..87c92a040b 100644 --- a/lib/blocs/file_download/personal_file_download_cubit.dart +++ b/lib/blocs/file_download/personal_file_download_cubit.dart @@ -42,6 +42,18 @@ class ProfileFileDownloadCubit extends FileDownloadCubit { _arfsRepository = arfsRepository, super(FileDownloadStarting()); + Future verifyUploadLimitationsAndDownload(SecretKey? cipherKey) async { + if (await AppPlatform.isSafari()) { + if (_file.size > publicDownloadSafariSizeLimit) { + emit(const FileDownloadFailure( + FileDownloadFailureReason.browserDoesNotSupportLargeDownloads)); + return; + } + } + + download(cipherKey); + } + Future download(SecretKey? cipherKey) async { try { final drive = await _arfsRepository.getDriveById(_file.driveId); @@ -127,7 +139,9 @@ class ProfileFileDownloadCubit extends FileDownloadCubit { String? cipherIvTag; SecretKey? fileKey; - if (drive.drivePrivacy == DrivePrivacy.private) { + final isPinFile = _file.pinnedDataOwnerAddress != null; + + if (drive.drivePrivacy == DrivePrivacy.private && !isPinFile) { SecretKey? driveKey; if (cipherKey != null) { @@ -138,9 +152,11 @@ class ProfileFileDownloadCubit extends FileDownloadCubit { } else { driveKey = await _driveDao.getDriveKeyFromMemory(_file.driveId); } + if (driveKey == null) { throw StateError('Drive Key not found'); } + fileKey = await _driveDao.getFileKey(_file.id, driveKey); final dataTx = await (_arweave.getTransactionDetails(_file.txId)); @@ -158,6 +174,7 @@ class ProfileFileDownloadCubit extends FileDownloadCubit { fileName: _file.name, fileSize: _file.size, lastModifiedDate: _file.lastModifiedDate, + isManifest: _file.contentType == ContentType.manifest, contentType: _file.contentType ?? lookupMimeTypeWithDefaultType(_file.name), cipher: cipher, @@ -170,13 +187,6 @@ class ProfileFileDownloadCubit extends FileDownloadCubit { return; } - if (progress == 100) { - emit(FileDownloadFinishedWithSuccess(fileName: _file.name)); - return; - } - - logger.d('Download progress: $progress'); - emit( FileDownloadWithProgress( fileName: _file.name, @@ -186,6 +196,7 @@ class ProfileFileDownloadCubit extends FileDownloadCubit { _file.contentType ?? lookupMimeTypeWithDefaultType(_file.name), ), ); + _downloadProgress.sink.add(FileDownloadProgress(progress / 100)); } diff --git a/lib/blocs/pin_file/pin_file_bloc.dart b/lib/blocs/pin_file/pin_file_bloc.dart index 9257cb8538..32dff49854 100644 --- a/lib/blocs/pin_file/pin_file_bloc.dart +++ b/lib/blocs/pin_file/pin_file_bloc.dart @@ -4,7 +4,6 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/core/arfs/entities/arfs_entities.dart'; import 'package:ardrive/core/crypto/crypto.dart'; import 'package:ardrive/entities/entities.dart' show FileEntity; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/misc/misc.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; diff --git a/lib/blocs/shared_file/shared_file_cubit.dart b/lib/blocs/shared_file/shared_file_cubit.dart index 67181ed8e5..b7d5d532fe 100644 --- a/lib/blocs/shared_file/shared_file_cubit.dart +++ b/lib/blocs/shared_file/shared_file_cubit.dart @@ -1,5 +1,4 @@ import 'package:ardrive/entities/entities.dart'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/utils/logger/logger.dart'; diff --git a/lib/blocs/sync/sync_cubit.dart b/lib/blocs/sync/sync_cubit.dart index 3f9709d360..ce3d71c798 100644 --- a/lib/blocs/sync/sync_cubit.dart +++ b/lib/blocs/sync/sync_cubit.dart @@ -7,7 +7,6 @@ import 'package:ardrive/blocs/constants.dart'; import 'package:ardrive/blocs/sync/ghost_folder.dart'; import 'package:ardrive/core/activity_tracker.dart'; import 'package:ardrive/entities/entities.dart'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/utils/logger/logger.dart'; diff --git a/lib/blocs/sync/utils/add_file_entity_revisions.dart b/lib/blocs/sync/utils/add_file_entity_revisions.dart index db1a08d096..f17802c451 100644 --- a/lib/blocs/sync/utils/add_file_entity_revisions.dart +++ b/lib/blocs/sync/utils/add_file_entity_revisions.dart @@ -29,17 +29,20 @@ Future> _addNewFileEntityRevisions({ continue; } // If Parent-Folder-Id is missing for a file, put it in the root folder + try { + entity.parentFolderId = entity.parentFolderId ?? rootPath; + final revision = + entity.toRevisionCompanion(performedAction: revisionPerformedAction); - entity.parentFolderId = entity.parentFolderId ?? rootPath; - final revision = - entity.toRevisionCompanion(performedAction: revisionPerformedAction); + if (revision.action.value.isEmpty) { + continue; + } - if (revision.action.value.isEmpty) { - continue; + newRevisions.add(revision); + latestRevisions[entity.id!] = revision; + } catch (e) { + logger.e('Error adding revision for entity: ${entity.id}', e); } - - newRevisions.add(revision); - latestRevisions[entity.id!] = revision; } await database.batch((b) { diff --git a/lib/blocs/upload/limits.dart b/lib/blocs/upload/limits.dart index e6efa9d72a..cebde0d219 100644 --- a/lib/blocs/upload/limits.dart +++ b/lib/blocs/upload/limits.dart @@ -1,16 +1,20 @@ -import 'package:ardrive/utils/data_size.dart'; -import 'package:flutter/foundation.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; -final privateFileSizeLimit = const MiB(100000).size; +final privateFileSizeLimit = const GiB(65).size; + +final largeFileUploadSizeThreshold = const MiB(500).size; final mobilePrivateFileSizeLimit = const GiB(10).size; final publicFileSafeSizeLimit = const GiB(5).size; +final nonChromeBrowserUploadSafeLimitUsingTurbo = const MiB(500).size; -final bundleSizeLimit = kIsWeb ? webBundleSizeLimit : mobileBundleSizeLimit; +int getBundleSizeLimit(bool isTurbo) => + isTurbo ? turboBundleSizeLimit : d2nBundleSizeLimit; -final webBundleSizeLimit = const MiB(65000).size; -final mobileBundleSizeLimit = const MiB(65000).size; +final d2nBundleSizeLimit = const GiB(65).size; +final turboBundleSizeLimit = const GiB(2).size; +final mobileBundleSizeLimit = const GiB(65).size; const maxBundleDataItemCount = 500; const maxFilesPerBundle = maxBundleDataItemCount ~/ 2; const maxFilesSizePerBundleUsingTurbo = 1; diff --git a/lib/blocs/upload/models/upload_plan.dart b/lib/blocs/upload/models/upload_plan.dart index cc2f08509b..41a6c889cf 100644 --- a/lib/blocs/upload/models/upload_plan.dart +++ b/lib/blocs/upload/models/upload_plan.dart @@ -5,7 +5,6 @@ import 'package:ardrive/blocs/upload/upload_handles/upload_handle.dart'; import 'package:ardrive/turbo/services/upload_service.dart'; import 'package:ardrive/utils/bundles/next_fit_bundle_packer.dart'; import 'package:ardrive/utils/logger/logger.dart'; -import 'package:flutter/foundation.dart'; import '../upload_handles/file_data_item_upload_handle.dart'; import '../upload_handles/file_v2_upload_handle.dart'; @@ -30,6 +29,7 @@ class UploadPlan { folderDataItemUploadHandles, required TurboUploadService turboUploadService, required int maxDataItemCount, + required bool useTurbo, }) async { final uploadPlan = UploadPlan._create( fileV2UploadHandles: fileV2UploadHandles, @@ -43,6 +43,7 @@ class UploadPlan { folderDataItemUploadHandles: folderDataItemUploadHandles, turboUploadService: turboUploadService, maxDataItemCount: maxDataItemCount, + useTurbo: useTurbo, ); } @@ -50,6 +51,7 @@ class UploadPlan { } Future createBundleHandlesFromDataItemHandles({ + required bool useTurbo, Map fileDataItemUploadHandles = const {}, Map folderDataItemUploadHandles = const {}, @@ -58,8 +60,7 @@ class UploadPlan { }) async { logger.i( 'Creating bundle handles from data item handles with a max number of files of $maxDataItemCount'); - final int maxBundleSize = - (kIsWeb ? bundleSizeLimit : mobileBundleSizeLimit); + final int maxBundleSize = getBundleSizeLimit(useTurbo); final folderItems = await NextFitBundlePacker( maxBundleSize: maxBundleSize, diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index e94fbac76c..c7a1bc4fc0 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -19,9 +19,11 @@ import 'package:ardrive/utils/upload_plan_utils.dart'; import 'package:ardrive_io/ardrive_io.dart'; import 'package:ardrive_uploader/ardrive_uploader.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:arweave/arweave.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pst/pst.dart'; import 'enums/conflicting_files_actions.dart'; @@ -90,8 +92,6 @@ class UploadCubit extends Cubit { final Map conflictingFiles = {}; final List conflictingFolders = []; - bool fileSizeWithinBundleLimits(int size) => size < bundleSizeLimit; - UploadCubit({ required this.driveId, required this.parentFolderId, @@ -322,7 +322,7 @@ class UploadCubit extends Cubit { _removeFilesWithFileNameConflicts(); } - logger.i( + logger.d( 'Upload preparation started. UploadMethod: $_uploadMethod', ); @@ -338,13 +338,12 @@ class UploadCubit extends Cubit { ), ); - _uploadMethod = uploadPreparation.uploadPaymentInfo.defaultPaymentMethod; - - logger.d('Upload method: $_uploadMethod'); - final paymentInfo = uploadPreparation.uploadPaymentInfo; final uploadPlansPreparation = uploadPreparation.uploadPlansPreparation; + _uploadMethod = paymentInfo.defaultPaymentMethod; + logger.d('Upload method: $_uploadMethod'); + if (await _profileCubit.checkIfWalletMismatch()) { emit(UploadWalletMismatch()); return; @@ -353,7 +352,7 @@ class UploadCubit extends Cubit { bool isTurboZeroBalance = uploadPreparation.uploadPaymentInfo.turboBalance == BigInt.zero; - logger.i( + logger.d( 'Upload preparation finished\n' 'UploadMethod: $_uploadMethod\n' 'UploadPlan For AR: ${uploadPreparation.uploadPaymentInfo.arCostEstimate.toString()}\n' @@ -364,8 +363,10 @@ class UploadCubit extends Cubit { 'Is Zero Balance: $isTurboZeroBalance\n', ); - final literalBalance = convertCreditsToLiteralString( + final literalBalance = convertWinstonToLiteralString( uploadPreparation.uploadPaymentInfo.turboBalance); + final literalARBalance = + convertWinstonToLiteralString(_auth.currentUser.walletBalance); bool isButtonEnabled = false; bool sufficientBalanceToPayWithAR = @@ -403,8 +404,7 @@ class UploadCubit extends Cubit { costEstimateAr: paymentInfo.arCostEstimate, costEstimateTurbo: paymentInfo.turboCostEstimate, credits: literalBalance, - arBalance: - convertCreditsToLiteralString(_auth.currentUser.walletBalance), + arBalance: literalARBalance, uploadIsPublic: _targetDrive.isPublic, sufficientArBalance: profile.walletBalance >= paymentInfo.arCostEstimate.totalCost, @@ -423,6 +423,7 @@ class UploadCubit extends Cubit { } bool hasEmittedError = false; + bool hasEmittedWarning = false; Future startUpload({ required UploadPlan uploadPlanForAr, @@ -438,7 +439,7 @@ class UploadCubit extends Cubit { logger.d('Max files per bundle: ${uploadPlan.maxDataItemCount}'); - logger.i('Starting upload...'); + logger.d('Starting upload...'); //Check if the same wallet it being used before starting upload. if (await _profileCubit.checkIfWalletMismatch()) { @@ -453,11 +454,26 @@ class UploadCubit extends Cubit { ), ); - logger.i( + logger.d( 'Wallet verified. Starting bundle preparation.... Number of bundles: ${uploadPlanForAr.bundleUploadHandles.length}. Number of V2 files: ${uploadPlanForAr.fileV2UploadHandles.length}'); if (configService.config.useNewUploader) { - logger.i('Uploading folder using the new uploader'); + if (_uploadMethod == UploadMethod.turbo) { + await _verifyIfUploadContainsLargeFilesUsingTurbo(); + if (!hasEmittedWarning && kIsWeb && !await AppPlatform.isChrome()) { + emit( + UploadShowingWarning( + reason: UploadWarningReason.fileTooLargeOnNonChromeBrowser, + uploadPlanForAR: uploadPlanForAr, + uploadPlanForTurbo: uploadPlanForTurbo, + ), + ); + hasEmittedWarning = true; + return; + } + } else { + _containsLargeTurboUpload = false; + } if (uploadFolders) { await _uploadFolderUsingArDriveUploader(); @@ -469,7 +485,7 @@ class UploadCubit extends Cubit { return; } - logger.i('Uploading using the old uploader'); + logger.d('Uploading using the old uploader'); final uploader = _getUploader(); await for (final progress in uploader.uploadFromHandles( @@ -484,7 +500,7 @@ class UploadCubit extends Cubit { ); } - logger.i('Upload finished'); + logger.d('Upload finished'); unawaited(_profileCubit.refreshBalance()); @@ -499,6 +515,10 @@ class UploadCubit extends Cubit { appInfoServices: AppInfoServices(), ), ), + arweave: Arweave( + gatewayUrl: Uri.parse(configService.config.defaultArweaveGatewayUrl!), + ), + pstService: _pst, ); final private = _targetDrive.isPrivate; @@ -564,10 +584,12 @@ class UploadCubit extends Cubit { (progress) { emit( UploadInProgressUsingNewUploader( - totalProgress: progress.progress, + totalProgress: progress.progressInPercentage, equatableBust: UniqueKey(), progress: progress, controller: uploadController, + uploadMethod: _uploadMethod!, + containsLargeTurboUpload: _containsLargeTurboUpload!, ), ); }, @@ -577,12 +599,22 @@ class UploadCubit extends Cubit { (tasks) async { logger.d('Upload finished'); + if (tasks.any((element) => element.status == UploadStatus.failed)) { + logger.e('Error uploading'); + // if any of the files failed, we should throw an error + addError(Exception('Error uploading')); + } + + final tasksWithSuccess = tasks + .where((element) => element.status == UploadStatus.complete) + .toList(); + try { final List foldersMetadata = []; final List filesMetadata = []; for (var metadata - in tasks.expand((element) => element.content ?? [])) { + in tasksWithSuccess.expand((element) => element.content ?? [])) { if (metadata is ARFSFolderUploadMetatadata) { foldersMetadata.add(metadata); } else if (metadata is ARFSFileUploadMetadata) { @@ -660,38 +692,31 @@ class UploadCubit extends Cubit { entity.txId = file.metadataTxId!; - try { - files.first.getIdentifier(); - // If path is a blob from drag and drop, use file name. Else use the path field from folder upload - // TODO: Changed this logic. PLEASE REVIEW IT. - if (revisionAction == RevisionAction.uploadNewVersion) { - final existingFile = await _driveDao - .fileById(driveId: driveId, fileId: file.id) - .getSingle(); - - final filePath = existingFile.path; - await _driveDao.writeFileEntity(entity, filePath); - await _driveDao.insertFileRevision( - entity.toRevisionCompanion( - performedAction: revisionAction, - ), - ); - } else { - logger.d(files.first.getIdentifier()); - final parentFolderPath = (await _driveDao - .folderById( - driveId: driveId, folderId: file.parentFolderId) - .getSingle()) - .path; - await _driveDao.writeFileEntity(entity, parentFolderPath); - await _driveDao.insertFileRevision( - entity.toRevisionCompanion( - performedAction: revisionAction, - ), - ); - } - } catch (e) { - logger.e('Error saving file', e); + if (revisionAction == RevisionAction.uploadNewVersion) { + final existingFile = await _driveDao + .fileById(driveId: driveId, fileId: file.id) + .getSingle(); + + final filePath = existingFile.path; + await _driveDao.writeFileEntity(entity, filePath); + await _driveDao.insertFileRevision( + entity.toRevisionCompanion( + performedAction: revisionAction, + ), + ); + } else { + logger.d(files.first.getIdentifier()); + final parentFolderPath = (await _driveDao + .folderById( + driveId: driveId, folderId: file.parentFolderId) + .getSingle()) + .path; + await _driveDao.writeFileEntity(entity, parentFolderPath); + await _driveDao.insertFileRevision( + entity.toRevisionCompanion( + performedAction: revisionAction, + ), + ); } } } catch (e) { @@ -704,13 +729,7 @@ class UploadCubit extends Cubit { ); } - void retryUploads(UploadController controller) { - controller.retryFailedTasks(_auth.currentUser.wallet); - } - - void retryTask(UploadController controller, UploadTask task) { - controller.retryTask(task, _auth.currentUser.wallet); - } + bool? _containsLargeTurboUpload; Future _uploadUsingArDriveUploader() async { final ardriveUploader = ArDriveUploader( @@ -720,6 +739,10 @@ class UploadCubit extends Cubit { appInfoServices: AppInfoServices(), ), ), + arweave: Arweave( + gatewayUrl: Uri.parse(configService.config.defaultArweaveGatewayUrl!), + ), + pstService: _pst, ); final private = _targetDrive.isPrivate; @@ -757,8 +780,6 @@ class UploadCubit extends Cubit { _uploadMethod == UploadMethod.ar ? UploadType.d2n : UploadType.turbo, ); - List completedTasks = []; - uploadController.onError((tasks) { logger.e('Error uploading', tasks); addError(Exception('Error uploading')); @@ -766,28 +787,17 @@ class UploadCubit extends Cubit { }); uploadController.onProgressChange( - (progress) { - final newCompletedTasks = progress.task.where( - (element) => - element.status == UploadStatus.complete && - !completedTasks.contains(element), - ); - - for (var element in newCompletedTasks) { - completedTasks.add(element); - // TODO: Save as the file is finished the upload - // _saveEntityOnDB(element); - for (var metadata in element.content!) { - logger.d(metadata.metadataTxId ?? 'METADATA IS NULL'); - } - } + (progress) async { + // TODO: Save as the file is finished the upload emit( UploadInProgressUsingNewUploader( progress: progress, - totalProgress: progress.progress, + totalProgress: progress.progressInPercentage, controller: uploadController, equatableBust: UniqueKey(), + uploadMethod: _uploadMethod!, + containsLargeTurboUpload: _containsLargeTurboUpload!, ), ); }, @@ -795,20 +805,22 @@ class UploadCubit extends Cubit { uploadController.onDone( (tasks) async { + logger.d('Upload finished'); + + for (var task in tasks) { + logger.d('Task status: ${task.status}'); + } + if (tasks.any((element) => element.status == UploadStatus.failed)) { - final progress = state as UploadInProgressUsingNewUploader; - emit( - UploadInProgressUsingNewUploader( - progress: progress.progress, - totalProgress: progress.progress.progress, - controller: uploadController, - equatableBust: UniqueKey(), - ), - ); - return; + // if any of the files failed, we should throw an error + logger.e('Error uploading'); + addError(Exception('Error uploading')); } - for (var task in tasks) { + logger.d('Saving files on database'); + + for (var task in tasks + .where((element) => element.status == UploadStatus.complete)) { await _saveEntityOnDB(task); } @@ -817,6 +829,19 @@ class UploadCubit extends Cubit { ); } + Future _verifyIfUploadContainsLargeFilesUsingTurbo() async { + if (_containsLargeTurboUpload == null) { + _containsLargeTurboUpload = false; + + for (var file in files) { + if (await file.ioFile.length >= largeFileUploadSizeThreshold) { + _containsLargeTurboUpload = true; + break; + } + } + } + } + Future _saveEntityOnDB(UploadTask task) async { // Single file only // TODO: abstract to the database interface. @@ -887,7 +912,7 @@ class UploadCubit extends Cubit { folderId: folderMetadata.id, ); - logger.i('Folder created with id: $id'); + logger.d('Folder created with id: $id'); entity.txId = metadata.metadataTxId!; @@ -911,7 +936,7 @@ class UploadCubit extends Cubit { final turboUploader = TurboUploader(_turbo, wallet); final arweaveUploader = ArweaveBundleUploader(_arweave.client); - logger.i( + logger.d( 'Uploaders created: Turbo: $turboUploader, Arweave: $arweaveUploader'); final bundleUploader = BundleUploader( @@ -926,7 +951,7 @@ class UploadCubit extends Cubit { bundleUploader: bundleUploader, fileV2Uploader: v2Uploader, prepareBundle: (handle) async { - logger.i( + logger.d( 'Preparing bundle.. using turbo: ${_uploadMethod == UploadMethod.turbo}'); await handle.prepareAndSignBundleTransaction( @@ -939,10 +964,10 @@ class UploadCubit extends Cubit { useTurbo: _uploadMethod == UploadMethod.turbo, ); - logger.i('Bundle preparation finished'); + logger.d('Bundle preparation finished'); }, prepareFile: (handle) async { - logger.i('Preparing file...'); + logger.d('Preparing file...'); await handle.prepareAndSignTransactions( arweaveService: _arweave, @@ -1003,7 +1028,11 @@ class UploadCubit extends Cubit { ); if (fileAboveWarningLimit) { - emit(UploadShowingWarning(reason: UploadWarningReason.fileTooLarge)); + emit(UploadShowingWarning( + reason: UploadWarningReason.fileTooLarge, + uploadPlanForAR: null, + uploadPlanForTurbo: null, + )); return; } @@ -1032,6 +1061,44 @@ class UploadCubit extends Cubit { logger.e('Failed to upload file', error, stackTrace); super.onError(error, stackTrace); } + + Future cancelUpload() async { + if (state is UploadInProgressUsingNewUploader) { + try { + final state = this.state as UploadInProgressUsingNewUploader; + + emit( + UploadInProgressUsingNewUploader( + controller: state.controller, + equatableBust: state.equatableBust, + progress: state.progress, + totalProgress: state.totalProgress, + isCanceling: true, + uploadMethod: _uploadMethod!, + containsLargeTurboUpload: state.containsLargeTurboUpload, + ), + ); + + await state.controller.cancel(); + + emit( + UploadInProgressUsingNewUploader( + controller: state.controller, + equatableBust: state.equatableBust, + progress: state.progress, + totalProgress: state.totalProgress, + isCanceling: false, + uploadMethod: _uploadMethod!, + containsLargeTurboUpload: state.containsLargeTurboUpload, + ), + ); + + emit(UploadCanceled()); + } catch (e) { + logger.e('Error canceling upload', e); + } + } + } } class UploadFolder extends IOFolder { diff --git a/lib/blocs/upload/upload_handles/bundle_upload_handle.dart b/lib/blocs/upload/upload_handles/bundle_upload_handle.dart index 7ad1e2cddb..a277ecd028 100644 --- a/lib/blocs/upload/upload_handles/bundle_upload_handle.dart +++ b/lib/blocs/upload/upload_handles/bundle_upload_handle.dart @@ -11,6 +11,7 @@ import 'package:ardrive/utils/logger/logger.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; import 'package:flutter/foundation.dart'; +import 'package:pst/pst.dart'; class BundleUploadHandle implements UploadHandle { final List fileDataItemUploadHandles; @@ -157,7 +158,7 @@ class BundleUploadHandle implements UploadHandle { for (var folder in folderDataItemUploadHandles) { await folder.writeFolderToDatabase(driveDao: driveDao); } - + for (var file in fileDataItemUploadHandles) { await file.writeFileEntityToDatabase( bundledInTxId: bundleId, diff --git a/lib/blocs/upload/upload_handles/file_v2_upload_handle.dart b/lib/blocs/upload/upload_handles/file_v2_upload_handle.dart index 2b0926e22a..86c872f678 100644 --- a/lib/blocs/upload/upload_handles/file_v2_upload_handle.dart +++ b/lib/blocs/upload/upload_handles/file_v2_upload_handle.dart @@ -11,6 +11,7 @@ import 'package:ardrive/services/services.dart'; import 'package:arweave/arweave.dart'; import 'package:cryptography/cryptography.dart'; import 'package:drift/drift.dart'; +import 'package:pst/pst.dart'; class FileV2UploadHandle implements UploadHandle { final FileEntity entity; diff --git a/lib/blocs/upload/upload_handles/folder_data_item_upload_handle.dart b/lib/blocs/upload/upload_handles/folder_data_item_upload_handle.dart index f76c145790..dd646f274d 100644 --- a/lib/blocs/upload/upload_handles/folder_data_item_upload_handle.dart +++ b/lib/blocs/upload/upload_handles/folder_data_item_upload_handle.dart @@ -3,9 +3,9 @@ import 'dart:convert'; import 'package:ardrive/blocs/upload/models/web_folder.dart'; import 'package:ardrive/blocs/upload/upload_handles/upload_handle.dart'; import 'package:ardrive/entities/folder_entity.dart'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; import 'package:cryptography/cryptography.dart'; @@ -65,7 +65,7 @@ class FolderDataItemUploadHandle implements UploadHandle, DataItemHandle { wallet, key: driveKey, ); - + await folderEntityTx.sign(wallet); } diff --git a/lib/blocs/upload/upload_state.dart b/lib/blocs/upload/upload_state.dart index 06e4a419e4..72f141428f 100644 --- a/lib/blocs/upload/upload_state.dart +++ b/lib/blocs/upload/upload_state.dart @@ -211,13 +211,19 @@ class UploadInProgressUsingNewUploader extends UploadState { final UploadProgress progress; final UploadController controller; final double totalProgress; + final bool isCanceling; final Key? equatableBust; + final UploadMethod uploadMethod; + final bool containsLargeTurboUpload; UploadInProgressUsingNewUploader({ required this.progress, required this.totalProgress, required this.controller, this.equatableBust, + this.isCanceling = false, + required this.uploadMethod, + required this.containsLargeTurboUpload, }); @override @@ -236,16 +242,27 @@ class UploadWalletMismatch extends UploadState {} class UploadShowingWarning extends UploadState { final UploadWarningReason reason; + final UploadPlan? uploadPlanForAR; + final UploadPlan? uploadPlanForTurbo; - UploadShowingWarning({required this.reason}); + UploadShowingWarning({ + required this.reason, + this.uploadPlanForAR, + this.uploadPlanForTurbo, + }); @override List get props => [reason]; } +class UploadCanceled extends UploadState {} + +class CancelD2NUploadWarning extends UploadState {} + enum UploadWarningReason { /// The user is attempting to upload a file that is too large. fileTooLarge, + fileTooLargeOnNonChromeBrowser, } enum UploadErrors { diff --git a/lib/components/create_manifest_form.dart b/lib/components/create_manifest_form.dart index b7a81d292a..a3ac607e19 100644 --- a/lib/components/create_manifest_form.dart +++ b/lib/components/create_manifest_form.dart @@ -1,7 +1,6 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/blocs/create_manifest/create_manifest_cubit.dart'; import 'package:ardrive/blocs/feedback_survey/feedback_survey_cubit.dart'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/misc/misc.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/drive_detail/components/hover_widget.dart'; @@ -14,9 +13,11 @@ import 'package:ardrive/utils/open_url.dart'; import 'package:ardrive/utils/usd_upload_cost_to_string.dart'; import 'package:ardrive/utils/validate_folder_name.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pst/pst.dart'; import '../utils/show_general_dialog.dart'; import 'components.dart'; diff --git a/lib/components/create_snapshot_dialog.dart b/lib/components/create_snapshot_dialog.dart index 509a1f1d20..d544404e84 100644 --- a/lib/components/create_snapshot_dialog.dart +++ b/lib/components/create_snapshot_dialog.dart @@ -3,11 +3,9 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/blocs/create_snapshot/create_snapshot_cubit.dart'; import 'package:ardrive/components/components.dart'; import 'package:ardrive/components/payment_method_selector_widget.dart'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/arweave/arweave.dart'; -import 'package:ardrive/services/config/config.dart'; -import 'package:ardrive/services/pst/pst.dart'; +import 'package:ardrive/services/config/config_service.dart'; import 'package:ardrive/theme/theme.dart'; import 'package:ardrive/turbo/services/payment_service.dart'; import 'package:ardrive/turbo/services/upload_service.dart'; @@ -20,6 +18,7 @@ import 'package:ardrive_ui/ardrive_ui.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pst/pst.dart'; Future promptToCreateSnapshot( BuildContext context, diff --git a/lib/components/details_panel.dart b/lib/components/details_panel.dart index cfb587e41d..5bf1d47bf6 100644 --- a/lib/components/details_panel.dart +++ b/lib/components/details_panel.dart @@ -9,7 +9,6 @@ import 'package:ardrive/components/truncated_address.dart'; import 'package:ardrive/core/arfs/entities/arfs_entities.dart'; import 'package:ardrive/core/crypto/crypto.dart'; import 'package:ardrive/download/multiple_file_download_modal.dart'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/l11n/l11n.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/drive_detail/components/drive_explorer_item_tile.dart'; diff --git a/lib/components/drive_attach_form.dart b/lib/components/drive_attach_form.dart index b4b6173c99..d8a761a66d 100644 --- a/lib/components/drive_attach_form.dart +++ b/lib/components/drive_attach_form.dart @@ -1,5 +1,4 @@ import 'package:ardrive/blocs/blocs.dart'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/user_interaction_wrapper.dart'; import 'package:ardrive/services/services.dart'; @@ -8,6 +7,7 @@ import 'package:ardrive/utils/add_debounce.dart'; import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:ardrive/utils/validate_folder_name.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:cryptography/cryptography.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/components/drive_detach_dialog.dart b/lib/components/drive_detach_dialog.dart index 6de59f36c0..bd59c7c535 100644 --- a/lib/components/drive_detach_dialog.dart +++ b/lib/components/drive_detach_dialog.dart @@ -1,8 +1,8 @@ import 'package:ardrive/blocs/drives/drives_cubit.dart'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:ardrive/utils/show_general_dialog.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/components/file_download_dialog.dart b/lib/components/file_download_dialog.dart index 3d86337058..6776a3b72f 100644 --- a/lib/components/file_download_dialog.dart +++ b/lib/components/file_download_dialog.dart @@ -37,12 +37,15 @@ Future promptToDownloadProfileFile({ ARFSFactory(), ), arDriveDownloader: ArDriveDownloader( - ardriveIo: ArDriveIO(), ioFileAdapter: IOFileAdapter()), + ardriveIo: ArDriveIO(), + ioFileAdapter: IOFileAdapter(), + arweave: arweave, + ), downloader: ArDriveMobileDownloader(), file: arfsFile, driveDao: context.read(), arweave: arweave, - )..download(cipherKey); + )..verifyUploadLimitationsAndDownload(cipherKey); return showArDriveDialog( context, barrierDismissible: false, @@ -72,12 +75,15 @@ Future promptToDownloadFileRevision({ ARFSFactory(), ), arDriveDownloader: ArDriveDownloader( - ardriveIo: ArDriveIO(), ioFileAdapter: IOFileAdapter()), + ardriveIo: ArDriveIO(), + ioFileAdapter: IOFileAdapter(), + arweave: arweave, + ), downloader: ArDriveMobileDownloader(), file: arfsFile, driveDao: context.read(), arweave: arweave, - )..download(cipherKey); + )..verifyUploadLimitationsAndDownload(cipherKey); return showArDriveDialog( context, @@ -142,6 +148,9 @@ class FileDownloadDialog extends StatelessWidget { } else if (state is FileDownloadFailure) { if (state.reason == FileDownloadFailureReason.unknownError) { return _fileDownloadFailedDialog(context); + } else if (state.reason == + FileDownloadFailureReason.browserDoesNotSupportLargeDownloads) { + return _fileDownloadFailedDueToAboveBrowserLimit(context); } return _fileDownloadFailedDueToFileAbovePrivateLimit(context); @@ -181,6 +190,21 @@ class FileDownloadDialog extends StatelessWidget { ); } + ArDriveStandardModal _fileDownloadFailedDueToAboveBrowserLimit( + BuildContext context) { + return _modalWrapper( + title: appLocalizationsOf(context).warningEmphasized, + description: + appLocalizationsOf(context).fileFailedToDownloadFileAbovePublicLimit, + actions: [ + ModalAction( + action: () => Navigator.pop(context), + title: appLocalizationsOf(context).ok, + ), + ], + ); + } + ArDriveStandardModal _warningToWaitDownloadFinishes(BuildContext context) { return _modalWrapper( title: appLocalizationsOf(context).warningEmphasized, diff --git a/lib/components/profile_card.dart b/lib/components/profile_card.dart index 554475b44a..d021b3557c 100644 --- a/lib/components/profile_card.dart +++ b/lib/components/profile_card.dart @@ -6,13 +6,13 @@ import 'package:ardrive/pages/drive_detail/components/hover_widget.dart'; import 'package:ardrive/services/arconnect/arconnect_wallet.dart'; import 'package:ardrive/turbo/services/payment_service.dart'; import 'package:ardrive/turbo/topup/components/turbo_balance_widget.dart'; +import 'package:ardrive/turbo/utils/utils.dart'; import 'package:ardrive/user/download_wallet/download_wallet_modal.dart'; import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:ardrive/utils/open_url_utils.dart'; import 'package:ardrive/utils/plausible_event_tracker.dart'; import 'package:ardrive/utils/truncate_string.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; -import 'package:arweave/utils.dart' as utils; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:responsive_builder/responsive_builder.dart'; @@ -274,10 +274,7 @@ class _ProfileCardState extends State { } Widget _buildBalanceRow(BuildContext context, ProfileLoggedIn state) { - final walletBalance = - double.tryParse(utils.winstonToAr(state.walletBalance)) - ?.toStringAsFixed(5) ?? - '0'; + final walletBalance = convertWinstonToLiteralString(state.walletBalance); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), diff --git a/lib/components/side_bar.dart b/lib/components/side_bar.dart index 37aab83cd9..5bf784c613 100644 --- a/lib/components/side_bar.dart +++ b/lib/components/side_bar.dart @@ -4,9 +4,12 @@ import 'package:ardrive/blocs/profile/profile_cubit.dart'; import 'package:ardrive/components/app_version_widget.dart'; import 'package:ardrive/components/new_button/new_button.dart'; import 'package:ardrive/components/theme_switcher.dart'; +import 'package:ardrive/dev_tools/app_dev_tools.dart'; +import 'package:ardrive/main.dart'; import 'package:ardrive/misc/resources.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/drive_detail/components/hover_widget.dart'; +import 'package:ardrive/services/config/config_service.dart'; import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:ardrive/utils/logger/logger.dart'; import 'package:ardrive/utils/open_url.dart'; @@ -113,6 +116,26 @@ class _AppSideBarState extends State { const SizedBox( height: 16, ), + if ((AppPlatform.isMobile || AppPlatform.isMobileWeb()) && + configService.flavor != Flavor.production) ...[ + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: GestureDetector( + child: Text( + 'Open dev tools', + style: ArDriveTypography.body + .buttonNormalBold() + .copyWith(fontWeight: FontWeight.w700), + ), + onTap: () { + ArDriveDevTools().showDevTools(); + }, + ), + ), + const SizedBox( + height: 16, + ), + ], const Padding( padding: EdgeInsets.only(left: 16.0), child: HelpButton(), diff --git a/lib/components/top_up_dialog.dart b/lib/components/top_up_dialog.dart index e547c9216d..3ba1a90b3a 100644 --- a/lib/components/top_up_dialog.dart +++ b/lib/components/top_up_dialog.dart @@ -621,7 +621,7 @@ class _BalanceViewState extends State<_BalanceView> { ), const SizedBox(height: 4), Text( - '${convertCreditsToLiteralString(widget.balance)} ${appLocalizationsOf(context).credits}', + '${convertWinstonToLiteralString(widget.balance)} ${appLocalizationsOf(context).credits}', style: ArDriveTypography.body.buttonXLargeBold( color: ArDriveTheme.of(context).themeData.colors.themeFgMuted, ), @@ -720,7 +720,7 @@ class PriceEstimateView extends StatelessWidget { Row( children: [ Text( - '$fiatCurrency $fiatAmount = ${convertCreditsToLiteralString(estimatedCredits)} ${appLocalizationsOf(context).credits}', + '$fiatCurrency $fiatAmount = ${convertWinstonToLiteralString(estimatedCredits)} ${appLocalizationsOf(context).credits}', style: ArDriveTypography.body.buttonNormalBold(), ), Transform.translate( diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index f218f6a711..098cd476b5 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -31,6 +31,8 @@ import 'package:ardrive_uploader/ardrive_uploader.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:loading_animation_widget/loading_animation_widget.dart'; +import 'package:pst/pst.dart'; import '../blocs/upload/upload_handles/bundle_upload_handle.dart'; import '../pages/drive_detail/components/drive_explorer_item_tile.dart'; @@ -143,6 +145,7 @@ class UploadForm extends StatefulWidget { class _UploadFormState extends State { final _scrollController = ScrollController(); + bool _isShowingCancelDialog = false; @override initState() { @@ -153,8 +156,10 @@ class _UploadFormState extends State { Widget build(BuildContext context) => BlocConsumer( listener: (context, state) async { if (state is UploadComplete || state is UploadWalletMismatch) { - Navigator.pop(context); - context.read().openRemindMe(); + if (!_isShowingCancelDialog) { + Navigator.pop(context); + context.read().openRemindMe(); + } } else if (state is UploadPreparationInitialized) { context.read().verifyFilesAboveWarningLimit(); } @@ -163,6 +168,7 @@ class _UploadFormState extends State { context.read().logoutProfile(); } }, + buildWhen: (previous, current) => current is! UploadComplete, builder: (context, state) { if (state is UploadFolderNameConflict) { return ArDriveStandardModal( @@ -702,6 +708,17 @@ class _UploadFormState extends State { ), ), ); + } else if (state is UploadCanceled) { + return ArDriveStandardModal( + title: 'Upload canceled', + description: 'Your upload was canceled', + actions: [ + ModalAction( + action: () => Navigator.of(context).pop(false), + title: appLocalizationsOf(context).okEmphasized, + ), + ], + ); } else if (state is UploadFailure) { if (state.error == UploadErrors.turboTimeout) { return ArDriveStandardModal( @@ -739,7 +756,11 @@ class _UploadFormState extends State { Text( appLocalizationsOf(context) .weDontRecommendUploadsAboveASafeLimit( - filesize(publicFileSafeSizeLimit), + filesize( + state.reason == UploadWarningReason.fileTooLarge + ? publicFileSafeSizeLimit + : nonChromeBrowserUploadSafeLimitUsingTurbo, + ), ), style: ArDriveTypography.body.buttonNormalRegular(), ), @@ -752,8 +773,19 @@ class _UploadFormState extends State { title: appLocalizationsOf(context).cancelEmphasized, ), ModalAction( - action: () => - context.read().checkFilesAboveLimit(), + action: () { + if (state.uploadPlanForAR != null && + state.reason == + UploadWarningReason + .fileTooLargeOnNonChromeBrowser) { + return context.read().startUpload( + uploadPlanForAr: state.uploadPlanForAR!, + uploadPlanForTurbo: state.uploadPlanForTurbo, + ); + } + + return context.read().checkFilesAboveLimit(); + }, title: appLocalizationsOf(context).proceed, ), ], @@ -768,6 +800,71 @@ class _UploadFormState extends State { }) { final progress = state.progress; return ArDriveStandardModal( + actions: [ + ModalAction( + action: () { + if (state.uploadMethod == UploadMethod.ar && + state.progress.task.any( + (element) => element.status == UploadStatus.inProgress)) { + _isShowingCancelDialog = true; + final cubit = context.read(); + + showAnimatedDialog( + context, + content: BlocBuilder( + bloc: cubit, + builder: (context, state) { + if (state is UploadComplete) { + // TODO: localize + return ArDriveStandardModal( + title: 'Upload complete', + description: + 'Your upload is complete. You can not cancel it anymore.', + actions: [ + ModalAction( + action: () { + // parent modal + Navigator.pop(context); + + Navigator.pop(context); + }, + title: 'Ok', + ), + ], + ); + } + // TODO: localize + return ArDriveStandardModal( + title: 'Warning', + description: + 'Cancelling this upload may still result in a charge to your wallet. Do you still wish to proceed?', + actions: [ + ModalAction( + action: () => Navigator.pop(context), + title: 'No', + ), + ModalAction( + action: () { + cubit.cancelUpload(); + Navigator.pop(context); + }, + title: 'Yes', + ), + ], + ); + }, + ), + ); + } else { + context.read().cancelUpload(); + } + }, + // TODO: localize + title: state.isCanceling + ? 'Canceling...' + : appLocalizationsOf(context).cancelEmphasized, + ), + ], width: kLargeDialogWidth, title: '${appLocalizationsOf(context).uploadingNFiles(state.progress.getNumberOfItems())} ${(state.totalProgress * 100).toStringAsFixed(2)}%', @@ -787,9 +884,10 @@ class _UploadFormState extends State { itemBuilder: (BuildContext context, int index) { final task = progress.task[index]; - String progressText; + String? progressText; String status = ''; + // TODO: localize switch (task.status) { case UploadStatus.notStarted: status = 'Not started'; @@ -800,8 +898,9 @@ class _UploadFormState extends State { case UploadStatus.paused: status = 'Paused'; break; - case UploadStatus.bundling: - status = 'Bundling'; + case UploadStatus.creatingMetadata: + status = + 'We are preparing your upload. Preparation step 1/2'; break; case UploadStatus.encryting: status = 'Encrypting'; @@ -815,18 +914,27 @@ class _UploadFormState extends State { case UploadStatus.preparationDone: status = 'Preparation done'; break; + case UploadStatus.canceled: + status = 'Canceled'; + break; + case UploadStatus.creatingBundle: + status = + 'We are preparing your upload. Preparation step 2/2'; } if (task.isProgressAvailable) { - if (task.uploadItem != null) { + if (task.status == UploadStatus.inProgress || + task.status == UploadStatus.complete || + task.status == UploadStatus.failed) { progressText = '${filesize(((task.uploadItem!.size) * task.progress).ceil())}/${filesize(task.uploadItem!.size)}'; - } else { - progressText = 'Preparing...'; } } else { - progressText = - 'Your upload is in progress, but for large files the progress it not available. Please wait...'; + if (task.status == UploadStatus.inProgress) { + // TODO: localize + progressText = + 'Your upload is in progress, but for large files the progress it not available. Please wait...'; + } } return Column( @@ -876,27 +984,33 @@ class _UploadFormState extends State { AnimatedSwitcher( duration: const Duration(seconds: 1), - child: Text( - status, + child: Column( + children: [ + Text( + status, + style: ArDriveTypography.body + .buttonNormalBold( + color: + ArDriveTheme.of(context) + .themeData + .colors + .themeFgOnDisabled, + ), + ), + ], + ), + ), + if (progressText != null) + Text( + progressText, style: ArDriveTypography.body - .buttonNormalBold( + .buttonNormalRegular( color: ArDriveTheme.of(context) .themeData .colors .themeFgOnDisabled, ), ), - ), - Text( - progressText, - style: ArDriveTypography.body - .buttonNormalRegular( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgOnDisabled, - ), - ), ], ), ), @@ -906,62 +1020,65 @@ class _UploadFormState extends State { crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment.end, children: [ - Flexible( - flex: 2, - child: ArDriveProgressBar( - height: 4, - indicatorColor: task.status == - UploadStatus.failed - ? ArDriveTheme.of(context) - .themeData - .colors - .themeErrorDefault - : task.progress == 1 - ? ArDriveTheme.of(context) + if (task.isProgressAvailable && + (task.status == + UploadStatus.failed || + task.status == + UploadStatus.inProgress || + task.status == + UploadStatus + .complete)) ...[ + Flexible( + flex: 2, + child: ArDriveProgressBar( + height: 4, + indicatorColor: + _getUploadStatusColor( + context, + task, + ), + percentage: task.progress, + ), + ), + Flexible( + child: Text( + '${(task.progress * 100).toInt()}%', + style: ArDriveTypography.body + .buttonNormalBold( + color: + ArDriveTheme.of(context) .themeData .colors - .themeSuccessDefault - : ArDriveTheme.of(context) + .themeFgDefault, + ), + ), + ), + ], + if (!task.isProgressAvailable || + task.status == + UploadStatus.creatingBundle || + task.status == + UploadStatus.creatingMetadata) + Flexible( + flex: 2, + child: SizedBox( + child: LoadingAnimationWidget + .prograssiveDots( + color: + ArDriveTheme.of(context) .themeData .colors .themeFgDefault, - percentage: task.progress, - ), - ), - Flexible( - child: Text( - '${(task.progress * 100).toInt()}%', - style: ArDriveTypography.body - .buttonNormalBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, + size: 40, + ), ), ), - ), const SizedBox( width: 8, ), - if (task.status == - UploadStatus.failed) - SizedBox( - height: 24, - child: ArDriveClickArea( - child: GestureDetector( - onTap: () { - context - .read() - .retryTask( - state.controller, - task, - ); - }, - child: ArDriveIcons.refresh(), - ), - ), - ) ], ), ), @@ -988,6 +1105,7 @@ class _UploadFormState extends State { const SizedBox( height: 8, ), + // TODO: localize Text( 'Total uploaded: ${filesize(state.progress.totalUploaded)} of ${filesize(state.progress.totalSize)}', style: ArDriveTypography.body @@ -998,6 +1116,7 @@ class _UploadFormState extends State { .themeFgDefault) .copyWith(fontWeight: FontWeight.bold), ), + // TODO: localize Text( 'Files uploaded: ${state.progress.tasksContentCompleted()} of ${state.progress.tasksContentLength()}', style: ArDriveTypography.body @@ -1008,14 +1127,57 @@ class _UploadFormState extends State { .themeFgDefault) .copyWith(fontWeight: FontWeight.bold), ), + // TODO: localize Text( 'Upload speed: ${filesize(state.progress.calculateUploadSpeed().toInt())}/s', style: ArDriveTypography.body.buttonNormalBold( color: ArDriveTheme.of(context).themeData.colors.themeFgDefault), ), + + if (state.containsLargeTurboUpload) ...[ + const SizedBox( + height: 8, + ), + Align( + alignment: Alignment.center, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Warning!', + style: ArDriveTypography.body + .buttonLargeBold( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeErrorMuted, + ) + .copyWith( + fontWeight: FontWeight.bold, + ), + ), + Text('Leaving this page may result in a failed upload', + style: ArDriveTypography.body.buttonLargeBold()) + ], + ), + ), + ], ], ), ); } + + Color _getUploadStatusColor( + BuildContext context, UploadTask uploadStatusColor) { + final themeColors = ArDriveTheme.of(context).themeData.colors; + + if (uploadStatusColor.status == UploadStatus.failed) { + return themeColors.themeErrorDefault; + } else if (uploadStatusColor.progress == 1) { + return themeColors.themeSuccessDefault; + } else { + return themeColors.themeFgDefault; + } + } } diff --git a/lib/core/crypto/crypto.dart b/lib/core/crypto/crypto.dart index 561fe1959f..ee95d31ff4 100644 --- a/lib/core/crypto/crypto.dart +++ b/lib/core/crypto/crypto.dart @@ -104,28 +104,42 @@ class ArDriveCrypto { final keyData = Uint8List.fromList(await key.extractBytes()); - final decryptedData = await decryptTransactionDataStream( - cipher, - cipherIv, - Stream.fromIterable([data]), - keyData, - data.length, - ); - - final bytes = await streamToUint8List(decryptedData); - logger.d('decryptedData: $bytes'); - - final jsonStr = utf8.decode(bytes); - - logger.d('json str: $jsonStr'); + Uint8List decryptedData; + + if (cipher == Cipher.aes256ctr) { + final stream = await decryptTransactionDataStream( + cipher, + cipherIv, + Stream.fromIterable([data]), + keyData, + data.length, + ); + + final bytes = await streamToUint8List(stream); + + decryptedData = bytes; + } else if (cipher == Cipher.aes256gcm) { + final secretBox = secretBoxFromDataWithMacConcatenation( + data, + nonce: cipherIv, + ); + + final decryptedDataAsListInt = await aesGcm.decrypt( + secretBox, + secretKey: key, + ); + + decryptedData = Uint8List.fromList(decryptedDataAsListInt); + } else { + throw TransactionDecryptionException(); + } + final jsonStr = utf8.decode(decryptedData); final jsonMap = json.decode(jsonStr); - logger.d('json map: $jsonMap'); - return jsonMap; - } catch (e) { - logger.e('Failed to decrypt entity json', e); + } catch (e, s) { + logger.e('Failed to decrypt entity json', e, s); throw TransactionDecryptionException(); } } @@ -209,19 +223,3 @@ class ProfileKeyDerivationResult { ProfileKeyDerivationResult(this.key, this.salt); } - -Future streamToUint8List(Stream stream) async { - List collectedData = await stream.toList(); - int totalLength = - collectedData.fold(0, (prev, element) => prev + element.length); - - final result = Uint8List(totalLength); - int offset = 0; - - for (var data in collectedData) { - result.setRange(offset, offset + data.length, data); - offset += data.length; - } - - return result; -} diff --git a/lib/core/download_service.dart b/lib/core/download_service.dart index a6f2a43537..acc2da6bc8 100644 --- a/lib/core/download_service.dart +++ b/lib/core/download_service.dart @@ -35,6 +35,11 @@ class _DownloadService implements DownloadService { @override Future>> downloadStream( String fileTxId, bool isManifest) async { + if (isManifest) { + final data = await download(fileTxId, true); + return Stream.fromIterable([data.toList()]); + } + final downloadResponse = await arweave.download(txId: fileTxId); return downloadResponse.$1; diff --git a/lib/core/upload/bundle_signer.dart b/lib/core/upload/bundle_signer.dart index 567995ce19..8fa2896466 100644 --- a/lib/core/upload/bundle_signer.dart +++ b/lib/core/upload/bundle_signer.dart @@ -1,9 +1,9 @@ import 'package:arconnect/arconnect.dart'; import 'package:ardrive/services/arweave/arweave_service.dart'; -import 'package:ardrive/services/pst/pst.dart'; import 'package:ardrive/utils/logger/logger.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; +import 'package:pst/pst.dart'; abstract class BundleSigner { Future signBundle({required DataBundle unSignedBundle}); diff --git a/lib/core/upload/cost_calculator.dart b/lib/core/upload/cost_calculator.dart index 5b8f36b6f8..5c7fd179d9 100644 --- a/lib/core/upload/cost_calculator.dart +++ b/lib/core/upload/cost_calculator.dart @@ -1,9 +1,9 @@ import 'package:ardrive/services/arweave/arweave.dart'; -import 'package:ardrive/services/pst/pst.dart'; import 'package:ardrive/turbo/turbo.dart'; import 'package:ardrive/utils/logger/logger.dart'; import 'package:arweave/utils.dart'; import 'package:equatable/equatable.dart'; +import 'package:pst/pst.dart'; abstract class ArDriveUploadCostCalculator { Future calculateCost({required int totalSize}); diff --git a/lib/core/upload/transaction_signer.dart b/lib/core/upload/transaction_signer.dart index 8f5cfcbdc3..3058383f51 100644 --- a/lib/core/upload/transaction_signer.dart +++ b/lib/core/upload/transaction_signer.dart @@ -2,13 +2,13 @@ import 'package:arconnect/arconnect.dart'; import 'package:ardrive/core/crypto/crypto.dart'; import 'package:ardrive/entities/entities.dart'; import 'package:ardrive/services/arweave/arweave_service.dart'; -import 'package:ardrive/services/pst/pst.dart'; import 'package:ardrive/utils/logger/logger.dart'; import 'package:ardrive_io/ardrive_io.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; import 'package:cryptography/cryptography.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:pst/pst.dart'; abstract class TransactionSigner { Future signTransaction({ diff --git a/lib/core/upload/uploader.dart b/lib/core/upload/uploader.dart index b1c35de288..e72fa9af98 100644 --- a/lib/core/upload/uploader.dart +++ b/lib/core/upload/uploader.dart @@ -379,7 +379,7 @@ class UploadPaymentEvaluator { .getSizeOfAllV2Files(uploadPlanForAR.fileV2UploadHandles); bool isUploadEligibleToTurbo = - uploadPlanForAR.fileV2UploadHandles.isEmpty && + uploadPlanForTurbo.fileV2UploadHandles.isEmpty && uploadPlanForTurbo.bundleUploadHandles.isNotEmpty; UploadCostEstimate turboCostEstimate = UploadCostEstimate.zero(); diff --git a/lib/dev_tools/app_dev_tools.dart b/lib/dev_tools/app_dev_tools.dart index 230c37b14f..026144db10 100644 --- a/lib/dev_tools/app_dev_tools.dart +++ b/lib/dev_tools/app_dev_tools.dart @@ -6,6 +6,7 @@ import 'package:ardrive/services/config/config.dart'; import 'package:ardrive/turbo/topup/blocs/payment_form/payment_form_bloc.dart'; import 'package:ardrive/utils/logger/logger.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -453,6 +454,7 @@ class AppConfigWindowManagerState extends State { ListView.separated( padding: const EdgeInsets.all(16), shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) => buildOption(options[index]), separatorBuilder: (context, index) => const SizedBox(height: 16), itemCount: options.length, @@ -573,8 +575,15 @@ class DraggableWindow extends HookWidget { @override Widget build(BuildContext context) { - final windowSize = useState(const Size(600, 600)); - final windowPos = useState(Offset.zero); + double height = 600; + double width = 600; + if (AppPlatform.isMobile) { + width = MediaQuery.of(context).size.width * 0.95; + height = MediaQuery.of(context).size.height * 0.8; + } + + final windowSize = useState(Size(width, height)); + final windowPos = useState(const Offset(5, 32)); final isWindowVisible = useState(true); if (!isWindowVisible.value) { diff --git a/lib/download/ardrive_downloader.dart b/lib/download/ardrive_downloader.dart index 213026580d..0b966dfa6a 100644 --- a/lib/download/ardrive_downloader.dart +++ b/lib/download/ardrive_downloader.dart @@ -2,8 +2,10 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:ardrive/blocs/blocs.dart'; +import 'package:ardrive/services/arweave/arweave_service.dart'; import 'package:ardrive/utils/logger/logger.dart'; import 'package:ardrive_crypto/ardrive_crypto.dart'; +import 'package:ardrive_http/ardrive_http.dart'; import 'package:ardrive_io/ardrive_io.dart'; import 'package:arweave/arweave.dart' as arweave; import 'package:arweave/utils.dart'; @@ -16,6 +18,7 @@ abstract class ArDriveDownloader { required String fileName, required DateTime lastModifiedDate, required String contentType, + required bool isManifest, Completer? cancelWithReason, SecretKey? fileKey, String? cipher, @@ -25,16 +28,18 @@ abstract class ArDriveDownloader { factory ArDriveDownloader({ required IOFileAdapter ioFileAdapter, required ArDriveIO ardriveIo, + required ArweaveService arweave, }) { - return _ArDriveDownloader(ioFileAdapter, ardriveIo); + return _ArDriveDownloader(ioFileAdapter, ardriveIo, arweave); } } class _ArDriveDownloader implements ArDriveDownloader { final IOFileAdapter _ioFileAdapter; final ArDriveIO _ardriveIo; + final ArweaveService _arweave; - _ArDriveDownloader(this._ioFileAdapter, this._ardriveIo); + _ArDriveDownloader(this._ioFileAdapter, this._ardriveIo, this._arweave); final Completer _cancelWithReason = Completer(); @@ -51,61 +56,72 @@ class _ArDriveDownloader implements ArDriveDownloader { required String fileName, required DateTime lastModifiedDate, required String contentType, + required bool isManifest, Completer? cancelWithReason, SecretKey? fileKey, String? cipher, String? cipherIvString, }) async* { - final streamDownloadResponse = await arweave.download( - txId: dataTx, - onProgress: (progress, speed) => logger.d(progress.toString()), - ); - - final streamDownload = streamDownloadResponse.$1; - Stream saveStream; - if (fileKey != null && cipher != null && cipherIvString != null) { - final cipherIv = decodeBase64ToBytes(cipherIvString); - - final keyData = Uint8List.fromList(await fileKey.extractBytes()); - - if (cipher == Cipher.aes256ctr) { - saveStream = await decryptTransactionDataStream( - cipher, - cipherIv, - streamDownload.transform(transformer), - keyData, - fileSize, - ); - } else if (cipher == Cipher.aes256gcm) { - List bytes = []; - - await for (var chunk in streamDownload) { - bytes.addAll(chunk); - yield bytes.length / fileSize * 100; - } + if (isManifest) { + final urlString = isManifest + ? '${_arweave.client.api.gatewayUrl.origin}/raw/$dataTx' + : '${_arweave.client.api.gatewayUrl.origin}/$dataTx'; - final encryptedData = await decryptTransactionData( - cipher, - cipherIvString, - Uint8List.fromList(bytes), - fileKey, - ); - - _ardriveIo.saveFile( - await IOFile.fromData(encryptedData, - name: fileName, - lastModifiedDate: lastModifiedDate, - contentType: contentType), - ); - - return; + final dataRes = await ArDriveHTTP().getAsBytes(urlString); + logger.i('Downloading manifest...'); + saveStream = Stream.fromIterable([dataRes.data]); + } else { + final streamDownloadResponse = await arweave.download( + txId: dataTx, + onProgress: (progress, speed) => logger.d(progress.toString()), + ); + + final streamDownload = streamDownloadResponse.$1; + + if (fileKey != null && cipher != null && cipherIvString != null) { + final cipherIv = decodeBase64ToBytes(cipherIvString); + + final keyData = Uint8List.fromList(await fileKey.extractBytes()); + + if (cipher == Cipher.aes256ctr) { + saveStream = await decryptTransactionDataStream( + cipher, + cipherIv, + streamDownload.transform(transformer), + keyData, + fileSize, + ); + } else if (cipher == Cipher.aes256gcm) { + List bytes = []; + + await for (var chunk in streamDownload) { + bytes.addAll(chunk); + yield bytes.length / fileSize * 100; + } + + final encryptedData = await decryptTransactionData( + cipher, + cipherIvString, + Uint8List.fromList(bytes), + fileKey, + ); + + _ardriveIo.saveFile( + await IOFile.fromData(encryptedData, + name: fileName, + lastModifiedDate: lastModifiedDate, + contentType: contentType), + ); + + return; + } else { + throw Exception('Unknown cipher: $cipher'); + } } else { - throw Exception('Unknown cipher: $cipher'); + saveStream = streamDownload.transform(transformer); } - } else { - saveStream = streamDownload.transform(transformer); } final file = await _ioFileAdapter.fromReadStreamGenerator( diff --git a/lib/download/limits.dart b/lib/download/limits.dart index 90c3bc195e..5cb217e31e 100644 --- a/lib/download/limits.dart +++ b/lib/download/limits.dart @@ -1,4 +1,3 @@ -import 'package:ardrive/utils/data_size.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; // ignore: depend_on_referenced_packages import 'package:device_info_plus/device_info_plus.dart'; @@ -8,6 +7,7 @@ import 'download_utils.dart'; final publicDownloadUnknownPlatformSizeLimit = const GiB(2).size; final publicDownloadWebSizeLimit = const MiB(500).size; final publicDownloadFirefoxSizeLimit = const GiB(2).size; +final publicDownloadSafariSizeLimit = const GiB(1).size; final publicDownloadMobileSizeLimit = const MiB(300).size; final privateDownloadUnknownPlatformSizeLimit = const GiB(2).size; diff --git a/lib/entities/manifest_data.dart b/lib/entities/manifest_data.dart index 5356a18f35..4b8eb2ceff 100644 --- a/lib/entities/manifest_data.dart +++ b/lib/entities/manifest_data.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:ardrive/entities/entities.dart'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/models/daos/drive_dao/drive_dao.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; @@ -53,7 +52,7 @@ class ManifestData { Uint8List get jsonData => utf8.encode(json.encode(this)) as Uint8List; Future asPreparedDataItem({ - required ArweaveAddress owner, + required ArweaveAddressString owner, }) async { final manifestDataItem = DataItem.withBlobData(data: jsonData) ..setOwner(owner) diff --git a/lib/entities/snapshot_entity.dart b/lib/entities/snapshot_entity.dart index 14cea599d9..6aeed31edb 100644 --- a/lib/entities/snapshot_entity.dart +++ b/lib/entities/snapshot_entity.dart @@ -1,7 +1,6 @@ import 'dart:typed_data'; import 'package:ardrive/core/crypto/crypto.dart'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/utils/logger/logger.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; @@ -86,7 +85,7 @@ class SnapshotEntity extends Entity { } Future asPreparedDataItem({ - required ArweaveAddress owner, + required ArweaveAddressString owner, }) async { final dataItem = DataItem() ..setOwner(owner) diff --git a/lib/main.dart b/lib/main.dart index f7a1682b29..6473212d8b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,10 +9,6 @@ import 'package:ardrive/components/keyboard_handler.dart'; import 'package:ardrive/core/activity_tracker.dart'; import 'package:ardrive/core/crypto/crypto.dart'; import 'package:ardrive/models/database/database_helpers.dart'; -import 'package:ardrive/pst/ardrive_contract_oracle.dart'; -import 'package:ardrive/pst/community_oracle.dart'; -import 'package:ardrive/pst/contract_oracle.dart'; -import 'package:ardrive/pst/contract_readers/verto_contract_reader.dart'; import 'package:ardrive/services/authentication/biometric_authentication.dart'; import 'package:ardrive/services/config/config_fetcher.dart'; import 'package:ardrive/theme/theme_switcher_bloc.dart'; @@ -43,6 +39,7 @@ import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:local_auth/local_auth.dart'; import 'package:provider/provider.dart'; +import 'package:pst/pst.dart'; import 'blocs/blocs.dart'; import 'firebase_options.dart'; @@ -205,11 +202,16 @@ class AppState extends State { RepositoryProvider( create: (_) => PstService( communityOracle: CommunityOracle( - ArDriveContractOracle([ - ContractOracle(VertoContractReader()), - // ContractOracle(RedstoneContractReader()), - // ContractOracle(SmartweaveContractReader()), - ]), + ArDriveContractOracle( + [ + ContractOracle(VertoContractReader()), + ContractOracle(RedstoneContractReader()), + ContractOracle(ARNSContractReader()), + ], + fallbackContractOracle: ContractOracle( + SmartweaveContractReader(), + ), + ), ), ), ), diff --git a/lib/models/daos/drive_dao/drive_dao.dart b/lib/models/daos/drive_dao/drive_dao.dart index c3e9f83401..da4dfad60a 100644 --- a/lib/models/daos/drive_dao/drive_dao.dart +++ b/lib/models/daos/drive_dao/drive_dao.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:ardrive/core/crypto/crypto.dart'; import 'package:ardrive/entities/entities.dart'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; diff --git a/lib/pages/drive_detail/components/drive_file_drop_zone.dart b/lib/pages/drive_detail/components/drive_file_drop_zone.dart index 9df36f46f2..43d0d48a24 100644 --- a/lib/pages/drive_detail/components/drive_file_drop_zone.dart +++ b/lib/pages/drive_detail/components/drive_file_drop_zone.dart @@ -22,6 +22,7 @@ import 'package:ardrive_ui/ardrive_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dropzone/flutter_dropzone.dart'; +import 'package:pst/pst.dart'; class DriveFileDropZone extends StatefulWidget { final String driveId; diff --git a/lib/pst/constants.dart b/lib/pst/constants.dart deleted file mode 100644 index e5477080fe..0000000000 --- a/lib/pst/constants.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:ardrive/types/transaction_id.dart'; - -final pstTransactionId = TransactionID( - '-8A6RexFkpfWwuyVO98wzSFZh0d6VJuI-buTJvlwOJQ', -); diff --git a/lib/pst/contract_readers/smartweave_contract_reader.dart b/lib/pst/contract_readers/smartweave_contract_reader.dart deleted file mode 100644 index 015f4a9997..0000000000 --- a/lib/pst/contract_readers/smartweave_contract_reader.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:ardrive/pst/contract_reader.dart'; -import 'package:ardrive/services/pst/implementations/pst_web.dart' - if (dart.library.io) 'package:ardrive/services/pst/implementations/pst_stub.dart' - as implementation; -import 'package:ardrive/types/transaction_id.dart'; - -class SmartweaveContractReader implements ContractReader { - @override - Future readContract(TransactionID txId) { - return implementation.readContract(txId); - } -} diff --git a/lib/services/app/app_info_services.dart b/lib/services/app/app_info_services.dart index 7f433e8b77..417c080309 100644 --- a/lib/services/app/app_info_services.dart +++ b/lib/services/app/app_info_services.dart @@ -48,4 +48,4 @@ class AppInfoServices { } const String appName = 'ArDrive-App'; -const String arfsVersion = '0.12'; +const String arfsVersion = '0.13'; diff --git a/lib/services/arweave/arweave_service.dart b/lib/services/arweave/arweave_service.dart index ba7de43596..ac7121c9d9 100644 --- a/lib/services/arweave/arweave_service.dart +++ b/lib/services/arweave/arweave_service.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'package:ardrive/core/crypto/crypto.dart'; import 'package:ardrive/entities/entities.dart'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/services/arweave/error/gateway_error.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/utils/arfs_txs_filter.dart'; @@ -1149,6 +1148,7 @@ class ArweaveService { dryRun: dryRun, ); + // TODO: replace with the method on ardrive_utils Future getArUsdConversionRateOrNull() async { try { return await getArUsdConversionRate(); @@ -1157,6 +1157,7 @@ class ArweaveService { } } + // TODO: replace with the method on ardrive_utils Future getArUsdConversionRate() async { const String coinGeckoApi = 'https://api.coingecko.com/api/v3/simple/price?ids=arweave&vs_currencies=usd'; diff --git a/lib/services/services.dart b/lib/services/services.dart index 3c327b3a92..37cf1a6750 100644 --- a/lib/services/services.dart +++ b/lib/services/services.dart @@ -1,4 +1,3 @@ export 'arconnect/arconnect.dart'; export 'arweave/arweave.dart'; export 'config/config.dart'; -export 'pst/pst.dart'; \ No newline at end of file diff --git a/lib/turbo/topup/blocs/payment_review/payment_review_bloc.dart b/lib/turbo/topup/blocs/payment_review/payment_review_bloc.dart index fad9f3a497..d365651c0f 100644 --- a/lib/turbo/topup/blocs/payment_review/payment_review_bloc.dart +++ b/lib/turbo/topup/blocs/payment_review/payment_review_bloc.dart @@ -205,7 +205,7 @@ class PaymentReviewBloc extends Bloc { ); } - String _getCreditsFromPaymentModel() => convertCreditsToLiteralString( + String _getCreditsFromPaymentModel() => convertWinstonToLiteralString( BigInt.from(int.parse(_paymentModel!.topUpQuote.winstonCreditAmount))); String? _getSubTotalFromPaymentModel() { diff --git a/lib/turbo/topup/components/turbo_balance_widget.dart b/lib/turbo/topup/components/turbo_balance_widget.dart index e5d55e080a..b62c1a8016 100644 --- a/lib/turbo/topup/components/turbo_balance_widget.dart +++ b/lib/turbo/topup/components/turbo_balance_widget.dart @@ -113,7 +113,7 @@ class _TurboBalanceState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - '${convertCreditsToLiteralString(balance)} ${appLocalizationsOf(context).credits}', + '${convertWinstonToLiteralString(balance)} ${appLocalizationsOf(context).credits}', style: ArDriveTypography.body.captionRegular().copyWith( fontWeight: FontWeight.w600, fontSize: 18, diff --git a/lib/turbo/topup/views/topup_payment_form.dart b/lib/turbo/topup/views/topup_payment_form.dart index 6c1b432561..9286d71496 100644 --- a/lib/turbo/topup/views/topup_payment_form.dart +++ b/lib/turbo/topup/views/topup_payment_form.dart @@ -238,7 +238,7 @@ class TurboPaymentFormViewState extends State { BlocBuilder( builder: (context, state) { return Text( - '${convertCreditsToLiteralString(state.winstonCredits)} Credits', + '${convertWinstonToLiteralString(state.winstonCredits)} Credits', style: ArDriveTypography.body.leadBold(), ); }, diff --git a/lib/turbo/turbo.dart b/lib/turbo/turbo.dart index ef8d74b57f..27e9aa8434 100644 --- a/lib/turbo/turbo.dart +++ b/lib/turbo/turbo.dart @@ -7,10 +7,10 @@ import 'package:ardrive/turbo/services/payment_service.dart'; import 'package:ardrive/turbo/topup/models/payment_model.dart'; import 'package:ardrive/turbo/topup/models/price_estimate.dart'; import 'package:ardrive/turbo/utils/storage_estimator.dart'; -import 'package:ardrive/utils/data_size.dart'; import 'package:ardrive/utils/disposable.dart'; import 'package:ardrive/utils/file_size_units.dart'; import 'package:ardrive/utils/logger/logger.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; import 'package:flutter_stripe/flutter_stripe.dart'; diff --git a/lib/turbo/utils/storage_estimator.dart b/lib/turbo/utils/storage_estimator.dart index 27b59365ae..a9d351b26d 100644 --- a/lib/turbo/utils/storage_estimator.dart +++ b/lib/turbo/utils/storage_estimator.dart @@ -1,5 +1,5 @@ -import 'package:ardrive/utils/data_size.dart'; import 'package:ardrive/utils/file_size_units.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; class FileStorageEstimator { static double computeStorageEstimateForCredits({ diff --git a/lib/turbo/utils/utils.dart b/lib/turbo/utils/utils.dart index cc05b469fb..561f8f37ee 100644 --- a/lib/turbo/utils/utils.dart +++ b/lib/turbo/utils/utils.dart @@ -1,4 +1,4 @@ -String convertCreditsToLiteralString(BigInt credits) { +String convertWinstonToLiteralString(BigInt credits) { final creditsAsAr = convertWinstonToAr(credits); final creditsString = creditsAsAr.toStringAsFixed(4); diff --git a/lib/utils/link_generators.dart b/lib/utils/link_generators.dart index 5544229004..4af8a51577 100644 --- a/lib/utils/link_generators.dart +++ b/lib/utils/link_generators.dart @@ -1,5 +1,5 @@ import 'package:ardrive/entities/constants.dart'; -import 'package:ardrive/entities/string_types.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/utils.dart'; import 'package:cryptography/cryptography.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/utils/snapshots/gql_drive_history.dart b/lib/utils/snapshots/gql_drive_history.dart index 46beb71364..e0ee64e220 100644 --- a/lib/utils/snapshots/gql_drive_history.dart +++ b/lib/utils/snapshots/gql_drive_history.dart @@ -1,8 +1,8 @@ -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/services/arweave/graphql/graphql_api.graphql.dart'; import 'package:ardrive/utils/snapshots/height_range.dart'; import 'package:ardrive/utils/snapshots/range.dart'; import 'package:ardrive/utils/snapshots/segmented_gql_data.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import '../../services/arweave/arweave_service.dart'; diff --git a/lib/utils/snapshots/snapshot_item.dart b/lib/utils/snapshots/snapshot_item.dart index ee8ff10225..353da7702f 100644 --- a/lib/utils/snapshots/snapshot_item.dart +++ b/lib/utils/snapshots/snapshot_item.dart @@ -1,12 +1,12 @@ import 'dart:async'; import 'dart:convert'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/services/arweave/arweave.dart'; import 'package:ardrive/utils/logger/logger.dart'; import 'package:ardrive/utils/snapshots/height_range.dart'; import 'package:ardrive/utils/snapshots/range.dart'; import 'package:ardrive/utils/snapshots/segmented_gql_data.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:stash/stash_api.dart'; import 'package:stash_memory/stash_memory.dart'; diff --git a/lib/utils/snapshots/snapshot_item_to_be_created.dart b/lib/utils/snapshots/snapshot_item_to_be_created.dart index a9c648fa27..719e9ab288 100644 --- a/lib/utils/snapshots/snapshot_item_to_be_created.dart +++ b/lib/utils/snapshots/snapshot_item_to_be_created.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/services/arweave/graphql/graphql_api.graphql.dart'; import 'package:ardrive/utils/snapshots/snapshot_types.dart'; import 'package:ardrive/utils/snapshots/tx_snapshot_to_snapshot_data.dart'; diff --git a/lib/utils/upload_plan_utils.dart b/lib/utils/upload_plan_utils.dart index 8eccab950a..457aab1e44 100644 --- a/lib/utils/upload_plan_utils.dart +++ b/lib/utils/upload_plan_utils.dart @@ -85,6 +85,8 @@ class UploadPlanUtils { ? RevisionAction.uploadNewVersion : RevisionAction.create; + final bundleSizeLimit = getBundleSizeLimit(useTurbo); + if (fileSize < bundleSizeLimit) { fileDataItemUploadHandles[fileEntity.id!] = FileDataItemUploadHandle( entity: fileEntity, @@ -129,6 +131,7 @@ class UploadPlanUtils { turboUploadService: turboUploadService, maxDataItemCount: useTurbo ? maxFilesSizePerBundleUsingTurbo : maxFilesPerBundle, + useTurbo: useTurbo, ); } diff --git a/packages/ardrive_crypto/lib/src/authenticate.dart b/packages/ardrive_crypto/lib/src/authenticate.dart index 051a99ee2b..28c7c519f2 100644 --- a/packages/ardrive_crypto/lib/src/authenticate.dart +++ b/packages/ardrive_crypto/lib/src/authenticate.dart @@ -1,3 +1,6 @@ +// TODO: this class could be useful. It's not used anywhere in the app, but it could be used to authenticate a transaction. +// The Authenticate class is tightly coupled with ArweaveService and TransactionCommonMixin + // import 'package:ardrive/services/arweave/graphql/graphql_api.graphql.dart'; // import 'package:ardrive/utils/data_size.dart'; // import 'package:arweave/arweave.dart'; diff --git a/packages/ardrive_crypto/lib/src/ciphers.dart b/packages/ardrive_crypto/lib/src/ciphers.dart index dd427bb867..0305338555 100644 --- a/packages/ardrive_crypto/lib/src/ciphers.dart +++ b/packages/ardrive_crypto/lib/src/ciphers.dart @@ -6,7 +6,7 @@ import 'package:ardrive_crypto/src/stream_aes.dart'; import 'package:ardrive_crypto/src/stream_cipher.dart'; import 'package:cryptography/cryptography.dart' hide Cipher; -StreamingCipher cipherBufferImpl(String cipherName) { +AesGcm cipherBufferImpl(String cipherName) { final impls = { Cipher.aes256gcm: AesGcm.with256bits(), // Avoid this implementation because it generates a 16 byte nonce by default... @@ -14,7 +14,7 @@ StreamingCipher cipherBufferImpl(String cipherName) { }; final impl = impls[cipherName]; if (impl == null) throw ArgumentError(); - return impl as StreamingCipher; + return impl; } FutureOr cipherStreamDecryptImpl( diff --git a/packages/ardrive_uploader/example/lib/main.dart b/packages/ardrive_uploader/example/lib/main.dart index ffd64b2ecc..50d5558dfb 100644 --- a/packages/ardrive_uploader/example/lib/main.dart +++ b/packages/ardrive_uploader/example/lib/main.dart @@ -130,7 +130,7 @@ class _UploadFormState extends State { wallet: wallet, ); controller?.onProgressChange((progress) { - _streamController.add(progress.progress); + _streamController.add(progress.progressInPercentage); }); setState(() { diff --git a/packages/ardrive_uploader/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/ardrive_uploader/example/macos/Flutter/GeneratedPluginRegistrant.swift index baaa2369d1..b55cc94d49 100644 --- a/packages/ardrive_uploader/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/packages/ardrive_uploader/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -15,6 +15,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) - FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } diff --git a/packages/ardrive_uploader/lib/ardrive_uploader.dart b/packages/ardrive_uploader/lib/ardrive_uploader.dart index 2e21b73de4..1427252ead 100644 --- a/packages/ardrive_uploader/lib/ardrive_uploader.dart +++ b/packages/ardrive_uploader/lib/ardrive_uploader.dart @@ -1,4 +1,3 @@ - library; export 'src/ardrive_uploader.dart'; diff --git a/packages/ardrive_uploader/lib/src/ardrive_uploader.dart b/packages/ardrive_uploader/lib/src/ardrive_uploader.dart index cdbb36ee14..51127bc368 100644 --- a/packages/ardrive_uploader/lib/src/ardrive_uploader.dart +++ b/packages/ardrive_uploader/lib/src/ardrive_uploader.dart @@ -7,6 +7,7 @@ import 'package:ardrive_uploader/src/streamed_upload.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; import 'package:cryptography/cryptography.dart' hide Cipher; +import 'package:pst/pst.dart'; enum UploadType { turbo, d2n } @@ -44,6 +45,8 @@ abstract class ArDriveUploader { factory ArDriveUploader({ ARFSUploadMetadataGenerator? metadataGenerator, required Uri turboUploadUri, + Arweave? arweave, + PstService? pstService, }) { metadataGenerator ??= ARFSUploadMetadataGenerator( tagsGenerator: ARFSTagsGenetator( @@ -51,10 +54,23 @@ abstract class ArDriveUploader { ), ); + arweave ??= Arweave(); + pstService ??= PstService( + communityOracle: CommunityOracle( + ArDriveContractOracle([ + ContractOracle(VertoContractReader()), + ContractOracle(RedstoneContractReader()), + ContractOracle(SmartweaveContractReader()), + ]), + ), + ); + return _ArDriveUploader( turboUploadUri: turboUploadUri, dataBundlerFactory: DataBundlerFactory(), metadataGenerator: metadataGenerator, + arweave: arweave, + pstService: pstService, ); } } @@ -64,13 +80,19 @@ class _ArDriveUploader implements ArDriveUploader { required DataBundlerFactory dataBundlerFactory, required ARFSUploadMetadataGenerator metadataGenerator, required Uri turboUploadUri, + required Arweave arweave, + required PstService pstService, }) : _dataBundlerFactory = dataBundlerFactory, _turboUploadUri = turboUploadUri, _metadataGenerator = metadataGenerator, + _arweave = arweave, + _pstService = pstService, _streamedUploadFactory = StreamedUploadFactory(); final StreamedUploadFactory _streamedUploadFactory; final DataBundlerFactory _dataBundlerFactory; + final Arweave _arweave; + final PstService _pstService; final ARFSUploadMetadataGenerator _metadataGenerator; final Uri _turboUploadUri; @@ -82,9 +104,17 @@ class _ArDriveUploader implements ArDriveUploader { SecretKey? driveKey, required UploadType type, }) async { - final dataBundler = _dataBundlerFactory.createDataBundler( - metadataGenerator: _metadataGenerator, - type: type, + final uploadController = UploadController( + StreamController(), + _streamedUploadFactory.fromUploadType(type, _turboUploadUri), + _dataBundlerFactory.createDataBundler( + metadataGenerator: _metadataGenerator, + type: type, + arweaveService: _arweave, + pstService: _pstService, + ), + numOfWorkers: 1, + maxTasksPerWorker: 1, ); final metadata = await _metadataGenerator.generateMetadata( @@ -92,31 +122,16 @@ class _ArDriveUploader implements ArDriveUploader { args, ); - final streamedUpload = - _streamedUploadFactory.fromUploadType(type, _turboUploadUri); - - final controller = UploadController( - StreamController(), - streamedUpload, + final uploadTask = ARFSUploadTask( + status: UploadStatus.notStarted, + content: [metadata], ); - await dataBundler.createDataBundle( - file: file, - metadata: metadata, - wallet: wallet, - driveKey: driveKey, - ); + uploadController.addTask(uploadTask); - streamedUpload.send( - UploadTask( - status: UploadStatus.notStarted, - content: [metadata], - ), - wallet, - controller, - ); + uploadController.sendTasks(wallet); - return controller; + return uploadController; } @override @@ -131,174 +146,47 @@ class _ArDriveUploader implements ArDriveUploader { final uploadController = UploadController( StreamController(), _streamedUploadFactory.fromUploadType(type, _turboUploadUri), + _dataBundlerFactory.createDataBundler( + metadataGenerator: _metadataGenerator, + type: type, + arweaveService: _arweave, + pstService: _pstService, + ), + numOfWorkers: driveKey != null ? 3 : 5, + maxTasksPerWorker: driveKey != null ? 1 : 5, ); - /// Attaches the upload controller to the upload service - _uploadFiles( - files: files, - wallet: wallet, - controller: uploadController, - driveKey: driveKey, - type: type, - ); - - return uploadController; - } - - Future _uploadFiles({ - required List<(ARFSUploadMetadataArgs, IOFile)> files, - required Wallet wallet, - SecretKey? driveKey, - required UploadController controller, - required UploadType type, - }) async { - List> activeUploads = []; - List contents = []; - List tasks = []; - int totalSize = 0; - for (var f in files) { + final ARFSUploadMetadataArgs metadataArgs = f.$1; + final IOFile ioFile = f.$2; + final metadata = await _metadataGenerator.generateMetadata( - f.$2, - f.$1, + ioFile, + metadataArgs, ); - final uploadTask = UploadTask( - status: UploadStatus.notStarted, + final fileTask = FileUploadTask( + file: ioFile, + metadata: metadata as ARFSFileUploadMetadata, content: [metadata], + encryptionKey: driveKey, + streamedUpload: _streamedUploadFactory.fromUploadType( + type, + _turboUploadUri, + ), ); - tasks.add(uploadTask); - - controller.updateProgress(task: uploadTask); - - contents.add(metadata); + uploadController.addTask(fileTask); } - for (int i = 0; i < files.length; i++) { - int fileSize = await files[i].$2.length; - - while (activeUploads.length >= 50 || - totalSize + fileSize >= 500 * 1024 * 1024) { - await Future.any(activeUploads); - - // Remove completed uploads and update totalSize - int recalculatedSize = 0; - List> ongoingUploads = []; - - for (var f in activeUploads) { - // You need to figure out how to get the file size for the ongoing upload here - // Add its size to recalculatedSize - int ongoingFileSize = await files[i].$2.length; - recalculatedSize += ongoingFileSize; - - ongoingUploads.add(f); - } - - activeUploads = ongoingUploads; - totalSize = recalculatedSize; - } + uploadController.updateProgress(); - totalSize += fileSize; + uploadController.sendTasks(wallet); - Future uploadFuture = _uploadSingleFile( - file: files[i].$2, - uploadController: controller, - wallet: wallet, - driveKey: driveKey, - metadata: contents[i], - uploadTask: tasks[i], - type: type, - ); - - uploadFuture.then((_) { - activeUploads.remove(uploadFuture); - totalSize -= fileSize; - }).catchError((error) { - activeUploads.remove(uploadFuture); - totalSize -= fileSize; - // TODO: Handle error - }); - - activeUploads.add(uploadFuture); - } - - await Future.wait(activeUploads); - } - - Future _uploadSingleFile({ - required IOFile file, - required UploadController uploadController, - required Wallet wallet, - SecretKey? driveKey, - required UploadTask uploadTask, - required ARFSUploadMetadata metadata, - required UploadType type, - }) async { - final dataBundler = _dataBundlerFactory.createDataBundler( - metadataGenerator: _metadataGenerator, - type: type, - ); - - final bdi = await dataBundler.createDataBundle( - file: file, - metadata: metadata, - wallet: wallet, - driveKey: driveKey, - onStartBundling: () { - uploadTask = uploadTask.copyWith( - status: UploadStatus.bundling, - ); - uploadController.updateProgress( - task: uploadTask, - ); - }, - onStartEncryption: () { - uploadTask = uploadTask.copyWith( - status: UploadStatus.encryting, - ); - uploadController.updateProgress( - task: uploadTask, - ); - }, - ); - - switch (type) { - case UploadType.d2n: - uploadTask = uploadTask.copyWith( - uploadItem: TransactionUploadTask( - data: bdi, - size: bdi.dataSize, - ), - ); - break; - case UploadType.turbo: - uploadTask = uploadTask.copyWith( - uploadItem: DataItemUploadTask( - data: bdi, - size: bdi.dataItemSize, - ), - status: UploadStatus.preparationDone, - ); - break; - } - - uploadController.updateProgress( - task: uploadTask, - ); - - final streamedUpload = - _streamedUploadFactory.fromUploadType(type, _turboUploadUri); - - final value = await streamedUpload - .send(uploadTask, wallet, uploadController) - .then((value) { - print('Upload complete'); - }).catchError((err) {}); - - return value; + return uploadController; } + // TODO: Check it @override Future uploadEntities({ required List<(ARFSUploadMetadataArgs, IOEntity)> entities, @@ -311,13 +199,19 @@ class _ArDriveUploader implements ArDriveUploader { final dataBundler = _dataBundlerFactory.createDataBundler( metadataGenerator: _metadataGenerator, type: type, + arweaveService: _arweave, + pstService: _pstService, ); + final streamedUpload = _streamedUploadFactory.fromUploadType( type, _turboUploadUri, ); - final entitiesWithMedata = <(ARFSUploadMetadata, IOEntity)>[]; + final filesWitMetadatas = <(ARFSFileUploadMetadata, IOFile)>[]; + final folderMetadatas = <(ARFSFolderUploadMetatadata, IOEntity)>[]; + + FolderUploadTask? folderUploadTask; for (var e in entities) { final metadata = await _metadataGenerator.generateMetadata( @@ -325,71 +219,59 @@ class _ArDriveUploader implements ArDriveUploader { e.$1, ); - entitiesWithMedata.add((metadata, e.$2)); + if (metadata is ARFSFolderUploadMetatadata) { + folderMetadatas.add((metadata, e.$2)); + continue; + } else if (metadata is ARFSFileUploadMetadata) { + filesWitMetadatas.add((metadata, e.$2 as IOFile)); + } } - final folderMetadatas = - entitiesWithMedata.where((element) => element.$2 is IOFolder).toList(); - final uploadController = UploadController( StreamController(), streamedUpload, + dataBundler, + numOfWorkers: driveKey != null ? 3 : 5, + maxTasksPerWorker: driveKey != null ? 1 : 5, ); if (folderMetadatas.isNotEmpty) { - final bundle = await dataBundler.createDataBundleForEntities( - entities: folderMetadatas, - wallet: wallet, - driveKey: driveKey, + folderUploadTask = FolderUploadTask( + folders: folderMetadatas, + content: folderMetadatas.map((e) => e.$1).toList(), + encryptionKey: driveKey, + streamedUpload: _streamedUploadFactory.fromUploadType( + type, + _turboUploadUri, + ), ); - /// folders always are generated in the first BDI. - final bundleForFolders = bundle.first; - - UploadTask folderBDITask = UploadTask( - status: UploadStatus.notStarted, - content: bundleForFolders.contents, - ); - - switch (type) { - case UploadType.turbo: - folderBDITask = folderBDITask.copyWith( - uploadItem: DataItemUploadTask( - size: bundleForFolders.dataItemResult.dataItemSize, - data: bundleForFolders.dataItemResult, - ), - status: UploadStatus.preparationDone, - ); - - case UploadType.d2n: - folderBDITask = folderBDITask.copyWith( - uploadItem: TransactionUploadTask( - data: bundleForFolders.dataItemResult, - size: bundleForFolders.dataItemResult.dataSize, - ), - ); - break; - } + uploadController.addTask(folderUploadTask); + } - uploadController.updateProgress( - task: folderBDITask, + for (var f in filesWitMetadatas) { + final fileTask = FileUploadTask( + file: f.$2, + metadata: f.$1, + encryptionKey: driveKey, + streamedUpload: _streamedUploadFactory.fromUploadType( + type, + _turboUploadUri, + ), + content: [f.$1], ); - // sends the upload - streamedUpload - .send(folderBDITask, wallet, uploadController) - .then((value) { - print('Upload complete'); - }).catchError((err) {}); + uploadController.addTask(fileTask); } - _uploadFiles( - files: entities.whereType<(ARFSUploadMetadataArgs, IOFile)>().toList(), - wallet: wallet, - driveKey: driveKey, - controller: uploadController, - type: type, - ); + if (folderUploadTask != null) { + // first sends the upload task for the folder and then uploads the files + uploadController.sendTask(folderUploadTask, wallet, onTaskCompleted: () { + uploadController.sendTasks(wallet); + }); + } else { + uploadController.sendTasks(wallet); + } return uploadController; } diff --git a/packages/ardrive_uploader/lib/src/constants.dart b/packages/ardrive_uploader/lib/src/constants.dart new file mode 100644 index 0000000000..8b1cdff998 --- /dev/null +++ b/packages/ardrive_uploader/lib/src/constants.dart @@ -0,0 +1,3 @@ +import 'package:ardrive_utils/ardrive_utils.dart'; + +final int maxSizeSupportedByGCMEncryption = MiB(100).size; diff --git a/packages/ardrive_uploader/lib/src/cost_calculator.dart b/packages/ardrive_uploader/lib/src/cost_calculator.dart new file mode 100644 index 0000000000..2d3fa64055 --- /dev/null +++ b/packages/ardrive_uploader/lib/src/cost_calculator.dart @@ -0,0 +1,108 @@ +import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:arweave/arweave.dart'; +import 'package:arweave/utils.dart'; +// ignore: depend_on_referenced_packages +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:pst/pst.dart'; + +abstract class ArDriveUploadCostCalculator { + Future calculateCost({required int totalSize}); +} + +class UploadCostEstimate extends Equatable { + final double? usdUploadCost; + + /// The fee amount provided to PST holders. + final BigInt pstFee; + + /// The sum of the upload cost and fees. + final BigInt totalCost; + + final int totalSize; + + const UploadCostEstimate({ + required this.pstFee, + required this.totalCost, + required this.totalSize, + required this.usdUploadCost, + }); + + factory UploadCostEstimate.zero() { + return UploadCostEstimate( + pstFee: BigInt.zero, + totalCost: BigInt.zero, + totalSize: 0, + usdUploadCost: 0, + ); + } + + @override + List get props => [ + usdUploadCost, + pstFee, + totalCost, + totalSize, + ]; +} + +class UploadCostEstimateCalculatorForAR extends ArDriveUploadCostCalculator { + final Arweave _arweave; + final PstService _pstService; + final ConvertArToUSD _arCostToUsd; + + UploadCostEstimateCalculatorForAR({ + required Arweave arweaveService, + required PstService pstService, + required ConvertArToUSD arCostToUsd, + }) : _arweave = arweaveService, + _arCostToUsd = arCostToUsd, + _pstService = pstService; + + @override + Future calculateCost({ + required int totalSize, + }) async { + final costInAR = await _arweave.api + .get('/price/$totalSize') + .then((res) => BigInt.parse(res.body)); + + final pstFee = await _pstService.getPSTFee(costInAR); + + final totalCostAR = costInAR + pstFee.value; + + final arUploadCost = winstonToAr(totalCostAR); + + debugPrint('Upload cost in AR: $arUploadCost'); + + final usdUploadCost = await _arCostToUsd.convertForUSD( + double.parse(arUploadCost), + ); + + return UploadCostEstimate( + pstFee: pstFee.value, + totalCost: totalCostAR, + totalSize: totalSize, + usdUploadCost: usdUploadCost, + ); + } +} + +abstract class ConvertForUSD { + Future convertForUSD(T value); +} + +class ConvertArToUSD implements ConvertForUSD { + @override + Future convertForUSD( + double arCost, + ) async { + final arUsdConversionRate = await getArUsdConversionRateOrNull(); + + if (arUsdConversionRate == null) { + return null; + } + + return arCost * arUsdConversionRate; + } +} diff --git a/packages/ardrive_uploader/lib/src/d2n_streamed_upload.dart b/packages/ardrive_uploader/lib/src/d2n_streamed_upload.dart index 1991b4fc5b..562212be64 100644 --- a/packages/ardrive_uploader/lib/src/d2n_streamed_upload.dart +++ b/packages/ardrive_uploader/lib/src/d2n_streamed_upload.dart @@ -3,40 +3,79 @@ import 'package:ardrive_uploader/src/streamed_upload.dart'; import 'package:arweave/arweave.dart'; class D2NStreamedUpload implements StreamedUpload { + UploadAborter? _aborter; + @override Future send( UploadTask handle, Wallet wallet, UploadController controller, ) async { - if (handle.uploadItem is! TransactionUploadTask) { + if (handle.uploadItem is! BundleTransactionUploadItem) { throw ArgumentError('handle must be of type TransactionUploadTask'); } + /// It is possible to cancel an upload before starting the network request. + if (_isCanceled) { + print('Upload canceled on D2NStreamedUpload'); + return; + } + print('D2NStreamedUpload.send'); - handle = handle.copyWith(status: UploadStatus.inProgress); + handle = handle.copyWith( + progress: 0, + status: UploadStatus.inProgress, + ); controller.updateProgress(task: handle); final progressStreamTask = await uploadTransaction( - (handle.uploadItem as TransactionUploadTask).data) + (handle.uploadItem as BundleTransactionUploadItem).data) .run(); - progressStreamTask.match((l) => print(''), (progressStream) async { - final listen = progressStream.listen( + progressStreamTask.match((l) { + handle = handle.copyWith(status: UploadStatus.failed); + controller.updateProgress(task: handle); + }, (uploadProgressAndAborter) async { + final uploadProgress = uploadProgressAndAborter.$1; + _aborter = uploadProgressAndAborter.$2; + final listen = uploadProgress.listen( (progress) { - // updates the progress. progress.$1 is the current chunk, progress.$2 is the total chunks - handle.progress = (progress.$1 / progress.$2); + final uploaded = progress.$1; + final total = progress.$2; + final progressPercent = uploaded / total; + + handle = handle.copyWith( + progress: progressPercent, + status: UploadStatus.inProgress, + ); + controller.updateProgress(task: handle); + + if (progress.$1 == progress.$2) { + print('D2NStreamedUpload.send.onDone'); + // finishes the upload + handle = handle.copyWith( + status: UploadStatus.complete, + progress: 1, + ); + + controller.updateProgress(task: handle); + } }, onDone: () { + print('D2NStreamedUpload.send.onDone'); // finishes the upload - handle = handle.copyWith(status: UploadStatus.complete, progress: 1); + handle = handle.copyWith( + status: UploadStatus.complete, + progress: 1, + ); controller.updateProgress(task: handle); }, onError: (e) { + print('D2NStreamedUpload.send.onError: $e'); handle = handle.copyWith( status: UploadStatus.failed, ); @@ -47,4 +86,15 @@ class D2NStreamedUpload implements StreamedUpload { listen.asFuture(); }); } + + /// Cancel D2N uploads are not supported yet. + @override + Future cancel(UploadTask handle, UploadController controller) async { + print('D2NStreamedUpload.cancel'); + _isCanceled = true; + + await _aborter?.abort(); + } + + bool _isCanceled = false; } diff --git a/packages/ardrive_uploader/lib/src/data_bundler.dart b/packages/ardrive_uploader/lib/src/data_bundler.dart index 6dd3cf5d2b..32995d7ecf 100644 --- a/packages/ardrive_uploader/lib/src/data_bundler.dart +++ b/packages/ardrive_uploader/lib/src/data_bundler.dart @@ -3,23 +3,39 @@ import 'dart:convert'; import 'package:ardrive_crypto/ardrive_crypto.dart'; import 'package:ardrive_io/ardrive_io.dart'; import 'package:ardrive_uploader/ardrive_uploader.dart'; +import 'package:ardrive_uploader/src/constants.dart'; +import 'package:ardrive_uploader/src/cost_calculator.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; import 'package:arweave/utils.dart'; import 'package:cryptography/cryptography.dart' hide Cipher; import 'package:flutter/foundation.dart'; import 'package:fpdart/fpdart.dart'; +import 'package:pst/pst.dart'; import 'package:uuid/uuid.dart'; class DataBundlerFactory { DataBundler createDataBundler({ required ARFSUploadMetadataGenerator metadataGenerator, required UploadType type, + required PstService pstService, + required Arweave arweaveService, }) { - if (type == UploadType.turbo) { - return BDIDataBundler(metadataGenerator); - } else { - return DataTransactionBundler(metadataGenerator); + switch (type) { + case UploadType.turbo: + return BDIDataBundler(metadataGenerator); + case UploadType.d2n: + return DataTransactionBundler( + metadataGenerator, + UploadCostEstimateCalculatorForAR( + arCostToUsd: ConvertArToUSD(), + arweaveService: arweaveService, + pstService: pstService, + ), + pstService, + ); + default: + throw Exception('Invalid upload type'); } } } @@ -30,8 +46,10 @@ abstract class DataBundler { required ARFSUploadMetadata metadata, required Wallet wallet, SecretKey? driveKey, - Function? onStartEncryption, - Function? onStartBundling, + Function? onStartMetadataCreation, + Function? onFinishMetadataCreation, + Function? onStartBundleCreation, + Function? onFinishBundleCreation, }); Future> createDataBundleForEntity({ @@ -51,8 +69,14 @@ abstract class DataBundler { class DataTransactionBundler implements DataBundler { final ARFSUploadMetadataGenerator metadataGenerator; + final UploadCostEstimateCalculatorForAR costCalculator; + final PstService pstService; - DataTransactionBundler(this.metadataGenerator); + DataTransactionBundler( + this.metadataGenerator, + this.costCalculator, + this.pstService, + ); @override Future createDataBundle({ @@ -62,6 +86,10 @@ class DataTransactionBundler implements DataBundler { SecretKey? driveKey, Function? onStartEncryption, Function? onStartBundling, + Function? onStartMetadataCreation, + Function? onFinishMetadataCreation, + Function? onStartBundleCreation, + Function? onFinishBundleCreation, }) async { if (driveKey != null) { onStartEncryption?.call(); @@ -75,30 +103,28 @@ class DataTransactionBundler implements DataBundler { fileLength: await file.length, metadata: metadata, wallet: wallet, - driveKey: driveKey, + encryptionKey: driveKey, ); + onStartMetadataCreation?.call(); + final metadataDataItem = await _generateMetadataDataItemForFile( metadata: metadata, dataStream: dataGenerator, - fileLength: await file.length, wallet: wallet, driveKey: driveKey, ); - print('file metadata: ${metadata.toJson()}'); - - for (var tag in metadata.dataItemTags) { - print('tag: ${tag.name} - ${tag.value}'); - } + onFinishMetadataCreation?.call(); final fileDataItem = _generateFileDataItem( metadata: metadata, dataStream: dataGenerator.$1, - fileLength: await file.length, - cipherIv: dataGenerator.$2, + fileLength: dataGenerator.$4, ); + onStartBundleCreation?.call(); + final transactionResult = await createDataBundleTransaction( dataItemFiles: [ metadataDataItem, @@ -108,6 +134,8 @@ class DataTransactionBundler implements DataBundler { tags: metadata.bundleTags.map((e) => createTag(e.name, e.value)).toList(), ); + onFinishBundleCreation?.call(); + return transactionResult; } @@ -262,11 +290,19 @@ class DataTransactionBundler implements DataBundler { print('Size of the bundled data item: $size bytes'); + final uploadCost = await costCalculator.calculateCost( + totalSize: size, + ); + + final target = await pstService.getWeightedPstHolder(); + return createTransactionTaskEither( + quantity: uploadCost.pstFee, wallet: wallet, dataStreamGenerator: r.stream, dataSize: size, tags: bundledDataItemTags, + target: target.toString(), ); }, ); @@ -291,8 +327,11 @@ class BDIDataBundler implements DataBundler { required ARFSUploadMetadata metadata, required Wallet wallet, SecretKey? driveKey, - Function? onStartEncryption, Function? onStartBundling, + Function? onStartMetadataCreation, + Function? onFinishMetadataCreation, + Function? onStartBundleCreation, + Function? onFinishBundleCreation, }) { return _createBundleStable( file: file, @@ -300,7 +339,10 @@ class BDIDataBundler implements DataBundler { wallet: wallet, driveKey: driveKey, onStartBundling: onStartBundling, - onStartEncryption: onStartEncryption, + onFinishBundleCreation: onFinishBundleCreation, + onStartBundleCreation: onStartBundleCreation, + onStartMetadataCreation: onStartMetadataCreation, + onFinishMetadataCreation: onFinishMetadataCreation, ); } @@ -308,15 +350,16 @@ class BDIDataBundler implements DataBundler { required IOFile file, required ARFSUploadMetadata metadata, required Wallet wallet, - Function? onStartEncryption, Function? onStartBundling, SecretKey? driveKey, + Function? onStartMetadataCreation, + Function? onFinishMetadataCreation, + Function? onStartBundleCreation, + Function? onFinishBundleCreation, }) async { - if (driveKey != null) { - onStartEncryption?.call(); - } else { - onStartBundling?.call(); - } + print('Creating bundle for file: ${file.path}'); + onStartMetadataCreation?.call(); + print('Creating metadata data item'); // returns the encrypted or not file read stream and the cipherIv if it was encrypted final dataGenerator = await _dataGenerator( @@ -324,24 +367,26 @@ class BDIDataBundler implements DataBundler { fileLength: await file.length, metadata: metadata, wallet: wallet, - driveKey: driveKey, + encryptionKey: driveKey, ); final metadataDataItem = await _generateMetadataDataItemForFile( metadata: metadata, dataStream: dataGenerator, - fileLength: await file.length, wallet: wallet, driveKey: driveKey, ); + onFinishMetadataCreation?.call(); + final fileDataItem = _generateFileDataItem( metadata: metadata, dataStream: dataGenerator.$1, - fileLength: await file.length, - cipherIv: dataGenerator.$2, + fileLength: dataGenerator.$4, ); + onStartBundleCreation?.call(); + final createBundledDataItem = createBundledDataItemTaskEither( dataItemFiles: [ metadataDataItem, @@ -353,9 +398,10 @@ class BDIDataBundler implements DataBundler { final bundledDataItem = await (await createBundledDataItem).run(); + onFinishBundleCreation?.call(); + return bundledDataItem.match((l) { - // TODO: handle error - print('Error: $l'); + print('Error bundling the file: $l'); print(StackTrace.current); throw l; }, (bdi) async { @@ -402,8 +448,7 @@ class BDIDataBundler implements DataBundler { final bundledDataItem = await (await createBundledDataItem).run(); return bundledDataItem.match((l) { - // TODO: handle error - print('Error: $l'); + print('Error bundling the file: $l'); print(StackTrace.current); throw l; }, (bdi) async { @@ -469,8 +514,7 @@ class BDIDataBundler implements DataBundler { // folder bdi final folderBDIResult = await folderBDITask.match((l) { - // TODO: handle error - print('Error: $l'); + print('Error bundling the folder bdi: $l'); print(StackTrace.current); throw l; }, (bdi) async { @@ -492,19 +536,9 @@ DataItemFile _generateFileDataItem({ required ARFSUploadMetadata metadata, required Stream Function() dataStream, required int fileLength, - Uint8List? cipherIv, }) { final tags = metadata.dataItemTags; - // if (cipherIv != null) { - // tags.add(Tag(EntityTag.cipher, Cipher.aes256ctr)); - // tags.add(Tag(EntityTag.cipherIv, encodeBytesToBase64(cipherIv))); - // } - - for (var tag in metadata.dataItemTags) { - print('tag: ${tag.name} - ${tag.value}'); - } - final dataItemFile = DataItemFile( dataSize: fileLength, streamGenerator: dataStream, @@ -524,10 +558,22 @@ Future _generateMetadataDataItem({ final metadataJson = metadata.toJson(); final metadataBytes = utf8.encode(jsonEncode(metadataJson)).map((e) => Uint8List.fromList([e])); + int length; if (driveKey != null) { + SecretKey key; + if (metadata is ARFSFolderUploadMetatadata) { + key = driveKey; + } else { + key = await deriveFileKey( + driveKey, + metadata.id, + keyByteLength, + ); + } + final encryptedMetadata = await handleEncryption( - driveKey, + key, () => Stream.fromIterable(metadataBytes), metadata.id, metadataBytes.length, @@ -536,18 +582,23 @@ Future _generateMetadataDataItem({ metadataStreamGenerator = encryptedMetadata.$1; final metadataCipherIv = encryptedMetadata.$2; + final cipher = encryptedMetadata.$3; metadata.entityMetadataTags .add(Tag(EntityTag.cipherIv, encodeBytesToBase64(metadataCipherIv!))); - metadata.entityMetadataTags.add(Tag(EntityTag.cipher, Cipher.aes256ctr)); + print('Encrypting metadata data item with cipher $cipher'); + + metadata.entityMetadataTags.add(Tag(EntityTag.cipher, cipher)); + length = encryptedMetadata.$4; } else { metadataStreamGenerator = () => Stream.fromIterable(metadataBytes); + length = metadataBytes.length; } final metadataTask = createDataItemTaskEither( wallet: wallet, dataStream: metadataStreamGenerator, - dataStreamSize: metadataBytes.length, + dataStreamSize: length, tags: metadata.entityMetadataTags .map((e) => createTag(e.name, e.value)) .toList(), @@ -556,7 +607,7 @@ Future _generateMetadataDataItem({ final metadataTaskEither = await metadataTask.run(); metadataTaskEither.match((l) { - print('Error: $l'); + print('Error creating metadata data item: $l'); print(StackTrace.current); throw l; }, (metadataDataItem) { @@ -565,7 +616,7 @@ Future _generateMetadataDataItem({ }); return DataItemFile( - dataSize: metadataBytes.length, + dataSize: length, streamGenerator: metadataStreamGenerator, tags: metadata.entityMetadataTags .map((e) => createTag(e.name, e.value)) @@ -575,30 +626,39 @@ Future _generateMetadataDataItem({ Future _generateMetadataDataItemForFile({ required ARFSUploadMetadata metadata, - required (Stream Function(), Uint8List? dataStream) dataStream, - required int fileLength, + required ( + Stream Function(), + Uint8List? dataStream, + String? cipher, + int fileLength + ) dataStream, required Wallet wallet, SecretKey? driveKey, }) async { final dataItemTags = metadata.dataItemTags; if (driveKey != null) { - dataItemTags.add(Tag(EntityTag.cipher, Cipher.aes256ctr)); - dataItemTags - .add(Tag(EntityTag.cipherIv, encodeBytesToBase64(dataStream.$2!))); + final cipher = dataStream.$3; + final cipherIv = dataStream.$2; + + dataItemTags.add(Tag(EntityTag.cipher, cipher!)); + dataItemTags.add(Tag(EntityTag.cipherIv, encodeBytesToBase64(cipherIv!))); } + final dataStreamGenerator = dataStream.$1; + final dataStreamSize = dataStream.$4; + final fileDataItemEither = createDataItemTaskEither( wallet: wallet, - dataStream: dataStream.$1, - dataStreamSize: fileLength, + dataStream: dataStreamGenerator, + dataStreamSize: dataStreamSize, tags: dataItemTags.map((e) => createTag(e.name, e.value)).toList(), ); final fileDataItemResult = await fileDataItemEither.run(); fileDataItemResult.match((l) { - print('Error: $l'); + print('Error: creating file data item: $l'); print(StackTrace.current); }, (fileDataItem) { metadata as ARFSFileUploadMetadata; @@ -606,6 +666,8 @@ Future _generateMetadataDataItemForFile({ metadata.setDataTxId = fileDataItem.id; }); + int metadataLength; + final metadataBytes = utf8 .encode(jsonEncode(metadata.toJson())) .map((e) => Uint8List.fromList([e])); @@ -613,25 +675,40 @@ Future _generateMetadataDataItemForFile({ Stream Function() metadataGenerator; if (driveKey != null) { + final fileKey = await deriveFileKey( + driveKey, + metadata.id, + keyByteLength, + ); + final result = await handleEncryption( - driveKey, - () => Stream.fromIterable(metadataBytes), - metadata.id, - metadataBytes.length, - keyByteLength); + fileKey, + () => Stream.fromIterable(metadataBytes), + metadata.id, + metadataBytes.length, + keyByteLength, + ); + metadataGenerator = result.$1; + metadataLength = result.$4; + + final metadataCipherIv = result.$2; + final metadataCipher = result.$3; metadata.entityMetadataTags - .add(Tag(EntityTag.cipherIv, encodeBytesToBase64(result.$2!))); - metadata.entityMetadataTags.add(Tag(EntityTag.cipher, AES256CTR)); + .add(Tag(EntityTag.cipherIv, encodeBytesToBase64(metadataCipherIv!))); + print('Encrypting metadata data item with cipher $metadataCipher'); + + metadata.entityMetadataTags.add(Tag(EntityTag.cipher, metadataCipher)); } else { metadataGenerator = () => Stream.fromIterable(metadataBytes); + metadataLength = metadataBytes.length; } final metadataTask = createDataItemTaskEither( wallet: wallet, dataStream: metadataGenerator, - dataStreamSize: metadataBytes.length, + dataStreamSize: metadataLength, tags: metadata.entityMetadataTags .map((e) => createTag(e.name, e.value)) .toList(), @@ -640,7 +717,7 @@ Future _generateMetadataDataItemForFile({ final metadataTaskEither = await metadataTask.run(); metadataTaskEither.match((l) { - print('Error: $l'); + print('Error: creating metadata data item: $l'); print(StackTrace.current); throw l; }, (metadataDataItem) { @@ -650,7 +727,7 @@ Future _generateMetadataDataItemForFile({ }); return DataItemFile( - dataSize: metadataBytes.length, + dataSize: metadataLength, streamGenerator: metadataGenerator, tags: metadata.entityMetadataTags .map((e) => createTag(e.name, e.value)) @@ -660,6 +737,8 @@ Future _generateMetadataDataItemForFile({ // ignore: constant_identifier_names const AES256CTR = Cipher.aes256ctr; +// ignore: constant_identifier_names +const AES256GCM = Cipher.aes256gcm; // ignore: non_constant_identifier_names final UNIT_BYTE_LIST = Uint8List(1); @@ -671,32 +750,79 @@ Future deriveFileKey( secretKey: driveKey, info: fileIdBytes, nonce: UNIT_BYTE_LIST); } -Future<(Stream Function(), Uint8List? cipherIv)> handleEncryption( - SecretKey driveKey, - Stream Function() dataStream, - String fileId, - int fileLength, - int keyByteLength) async { - final fileKey = await deriveFileKey(driveKey, fileId, keyByteLength); - final keyData = Uint8List.fromList(await fileKey.extractBytes()); - final impl = await cipherStreamEncryptImpl(AES256CTR, keyData: keyData); - final encryptStreamResult = - await impl.encryptStreamGenerator(dataStream, fileLength); - return (encryptStreamResult.streamGenerator, encryptStreamResult.nonce); +Future< + ( + Stream Function(), + Uint8List? cipherIv, + String cipher, + int fileLength + )> handleEncryption( + SecretKey encryptionKey, + Stream Function() dataStream, + String fileId, + int fileLength, + int keyByteLength, +) async { + final keyData = Uint8List.fromList(await encryptionKey.extractBytes()); + Stream Function() dataStreamGenerator; + Uint8List nonce; + String cipher; + int length; + print('File length before encryption: $fileLength'); + + if (fileLength < maxSizeSupportedByGCMEncryption) { + // uses GCM + final impl = cipherBufferImpl(AES256GCM); + cipher = AES256GCM; + final data = await streamToUint8List(dataStream()); + final encryptStreamResult = + await impl.encrypt(data.toList(), secretKey: encryptionKey); + final encryptedData = encryptStreamResult.concatenation(nonce: false); + dataStreamGenerator = () => Stream.fromIterable([encryptedData]); + nonce = Uint8List.fromList(encryptStreamResult.nonce); + length = encryptedData.length; + } else { + final impl = await cipherStreamEncryptImpl(AES256CTR, keyData: keyData); + final encryptStreamResult = + await impl.encryptStreamGenerator(dataStream, fileLength); + cipher = AES256CTR; + dataStreamGenerator = encryptStreamResult.streamGenerator; + nonce = encryptStreamResult.nonce; + length = fileLength; + } + + print('File length after encryption: $length'); + + return ( + dataStreamGenerator, + nonce, + cipher, + length, + ); } -Future<(Stream Function() generator, Uint8List? cipherIv)> - _dataGenerator({ +Future< + ( + Stream Function() generator, + Uint8List? cipherIv, + String? cipher, + int fileSize + )> _dataGenerator({ required ARFSUploadMetadata metadata, required Stream Function() dataStream, required int fileLength, required Wallet wallet, - SecretKey? driveKey, + SecretKey? encryptionKey, }) async { - if (driveKey != null) { + if (encryptionKey != null) { return await handleEncryption( - driveKey, dataStream, metadata.id, fileLength, keyByteLength); + encryptionKey, dataStream, metadata.id, fileLength, keyByteLength); } else { - return (dataStream, null); + return ( + dataStream, + null, + null, + fileLength, + ); } } diff --git a/packages/ardrive_uploader/lib/src/streamed_upload.dart b/packages/ardrive_uploader/lib/src/streamed_upload.dart index f9babaf9d6..c7f2c7c711 100644 --- a/packages/ardrive_uploader/lib/src/streamed_upload.dart +++ b/packages/ardrive_uploader/lib/src/streamed_upload.dart @@ -10,6 +10,8 @@ abstract class StreamedUpload { Wallet wallet, UploadController controller, ); + + Future cancel(T handle, UploadController controller); } class StreamedUploadFactory { diff --git a/packages/ardrive_uploader/lib/src/turbo_streamed_upload.dart b/packages/ardrive_uploader/lib/src/turbo_streamed_upload.dart index d90b775075..aa8fe4c647 100644 --- a/packages/ardrive_uploader/lib/src/turbo_streamed_upload.dart +++ b/packages/ardrive_uploader/lib/src/turbo_streamed_upload.dart @@ -18,7 +18,7 @@ class TurboStreamedUpload implements StreamedUpload { @override Future send( - handle, + uploadTask, Wallet wallet, UploadController controller, ) async { @@ -41,14 +41,31 @@ class TurboStreamedUpload implements StreamedUpload { }, ); - handle = handle.copyWith(status: UploadStatus.inProgress); - controller.updateProgress(task: handle); + int size = 0; + + final task = uploadTask.uploadItem!.data as DataItemResult; + + await for (final data in task.streamGenerator()) { + size += data.length; + } - if (kIsWeb && handle.uploadItem!.size > 1024 * 1024 * 500) { - handle.isProgressAvailable = false; - controller.updateProgress(task: handle); + /// It is possible to cancel an upload before starting the network request. + if (_isCanceled) { + print('Upload canceled on StreamedUpload'); + return; } + /// If the file is larger than 500 MiB, we don't get progress updates. + /// + /// The TurboUploadServiceImpl for web uses fetch_client for the upload of files + /// larger than 500 MiB. fetch_client does not support progress updates. + if (kIsWeb && uploadTask.uploadItem!.size > MiB(500).size) { + uploadTask = uploadTask.copyWith( + isProgressAvailable: false, status: UploadStatus.inProgress); + } + + controller.updateProgress(task: uploadTask); + // gets the streamed request final streamedRequest = _turbo .postStream( @@ -58,31 +75,51 @@ class TurboStreamedUpload implements StreamedUpload { 'x-address': publicKey, 'x-signature': signature, }, - dataItem: handle.uploadItem!.data, - size: handle.uploadItem!.size, + dataItem: uploadTask.uploadItem!.data, + size: size, onSendProgress: (progress) { - handle.progress = progress; - controller.updateProgress(task: handle); + uploadTask = uploadTask.copyWith( + progress: progress, + status: UploadStatus.inProgress, + ); + controller.updateProgress(task: uploadTask); }) .then((value) async { - print('value: $value'); - if (!handle.isProgressAvailable) { - print('Progress is not available, setting to 1'); - handle.progress = 1; + if (!uploadTask.isProgressAvailable) { + uploadTask = uploadTask.copyWith( + progress: 1, + status: UploadStatus.complete, + ); } - handle.status = UploadStatus.complete; + uploadTask = uploadTask.copyWith( + status: UploadStatus.complete, + ); - controller.updateProgress(task: handle); + controller.updateProgress(task: uploadTask); return value; }).onError((e, s) { - print(e.toString()); - handle.status = UploadStatus.failed; - print('handle.status: ${handle.status}'); - controller.updateProgress(task: handle); + print('Error on TurboStreamedUpload.send: $e'); + uploadTask = uploadTask.copyWith( + status: UploadStatus.failed, + ); + controller.updateProgress(task: uploadTask); }); return streamedRequest; } + + @override + Future cancel( + UploadTask handle, + UploadController controller, + ) async { + _isCanceled = true; + await _turbo.cancel(); + handle = handle.copyWith(status: UploadStatus.canceled); + controller.updateProgress(task: handle); + } + + bool _isCanceled = false; } diff --git a/packages/ardrive_uploader/lib/src/turbo_upload_service_base.dart b/packages/ardrive_uploader/lib/src/turbo_upload_service_base.dart index 7f1b4fdb00..3f1e3df94d 100644 --- a/packages/ardrive_uploader/lib/src/turbo_upload_service_base.dart +++ b/packages/ardrive_uploader/lib/src/turbo_upload_service_base.dart @@ -12,5 +12,5 @@ abstract class TurboUploadService { required Map headers, }); - bool get isPossibleGetProgress; + Future cancel(); } diff --git a/packages/ardrive_uploader/lib/src/turbo_upload_service_dart_io.dart b/packages/ardrive_uploader/lib/src/turbo_upload_service_dart_io.dart index 33c18b6383..5536374eab 100644 --- a/packages/ardrive_uploader/lib/src/turbo_upload_service_dart_io.dart +++ b/packages/ardrive_uploader/lib/src/turbo_upload_service_dart_io.dart @@ -15,6 +15,8 @@ class TurboUploadServiceImpl implements TurboUploadService { required this.turboUploadUri, }); + CancelToken _cancelToken = CancelToken(); + /// We are using Dio directly here. In the future we must adapt our ArDriveHTTP to support /// streaming uploads. /// This is a temporary solution. @@ -28,37 +30,47 @@ class TurboUploadServiceImpl implements TurboUploadService { }) async { final url = '$turboUploadUri/v1/tx'; - int dataItemSize = 0; - - await for (final data in dataItem.streamGenerator()) { - dataItemSize += data.length; - } - final dio = Dio(); - final response = await dio.post( - url, - onSendProgress: (sent, total) { - print('Sent: $sent, total: $total'); - onSendProgress?.call(sent / total); - }, - data: dataItem.streamGenerator(), // Creates a Stream>. - options: Options( - headers: { - // stream - Headers.contentTypeHeader: 'application/octet-stream', - Headers.contentLengthHeader: dataItemSize, // Set the content-length. - }..addAll(headers), - ), - ); + try { + final response = await dio.post( + url, + onSendProgress: (sent, total) { + onSendProgress?.call(sent / total); + }, + data: dataItem.streamGenerator(), // Creates a Stream>. + options: Options( + headers: { + // stream + Headers.contentTypeHeader: 'application/octet-stream', + Headers.contentLengthHeader: size, // Set the content-length. + }..addAll(headers), + ), + cancelToken: _cancelToken, + ); + print('Response from turbo: ${response.statusCode}'); - print('Response from turbo: ${response.statusCode}'); + return response; + } catch (e) { + if (_isCanceled) { + _cancelToken = CancelToken(); - return response; + _cancelToken.cancel(); + } + + rethrow; + } } @override - bool get isPossibleGetProgress => true; + Future cancel() { + _cancelToken.cancel(); + print('Stream closed'); + _isCanceled = true; + return Future.value(); + } + + bool _isCanceled = false; } class TurboUploadExceptions implements Exception {} diff --git a/packages/ardrive_uploader/lib/src/turbo_upload_service_web.dart b/packages/ardrive_uploader/lib/src/turbo_upload_service_web.dart index aa21628f03..add7b37119 100644 --- a/packages/ardrive_uploader/lib/src/turbo_upload_service_web.dart +++ b/packages/ardrive_uploader/lib/src/turbo_upload_service_web.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:ardrive_uploader/src/turbo_upload_service_base.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; import 'package:dio/dio.dart'; import 'package:fetch_client/fetch_client.dart'; @@ -19,6 +20,15 @@ class TurboUploadServiceImpl implements TurboUploadService { required this.turboUploadUri, }); + final _fetchController = StreamController>(sync: false); + CancelToken _cancelToken = CancelToken(); + + final client = FetchClient( + mode: RequestMode.cors, + streamRequests: true, + cache: RequestCache.noCache, + ); + @override Future postStream({ required DataItemResult dataItem, @@ -28,12 +38,7 @@ class TurboUploadServiceImpl implements TurboUploadService { required Map headers, }) { // max of 500mib - if (dataItem.dataItemSize <= 1024 * 1024 * 500) { - _isPossibleGetProgress = true; - - // TODO: Add this to the task instead of the controller - // controller.isPossibleGetProgress = true; - + if (dataItem.dataItemSize <= MiB(500).size) { return _uploadWithDio( dataItem: dataItem, wallet: wallet, @@ -61,33 +66,36 @@ class TurboUploadServiceImpl implements TurboUploadService { }) async { final url = '$turboUploadUri/v1/tx'; - int dataItemSize = 0; - - // TODO: remove after fixing the issue with the size of the upload - await for (final data in dataItem.streamGenerator()) { - dataItemSize += data.length; - } - final dio = Dio(); - - final response = await dio.post( - url, - onSendProgress: (sent, total) { - onSendProgress?.call(sent / total); - }, - data: dataItem.streamGenerator(), // Creates a Stream>. - options: Options( - headers: { - // stream - Headers.contentTypeHeader: 'application/octet-stream', - Headers.contentLengthHeader: dataItemSize, // Set the content-length. - }..addAll(headers), - ), - ); - - print('Response from turbo: ${response.statusCode}'); - - return response; + try { + final response = await dio.post( + url, + onSendProgress: (sent, total) { + onSendProgress?.call(sent / total); + }, + data: dataItem.streamGenerator(), // Creates a Stream>. + options: Options( + headers: { + // stream + Headers.contentTypeHeader: 'application/octet-stream', + Headers.contentLengthHeader: size, // Set the content-length. + }..addAll(headers), + ), + cancelToken: _cancelToken, + ); + + print('Response from turbo: ${response.statusCode}'); + + return response; + } catch (e) { + print('Error on turbo upload: $e'); + if (_isCanceled) { + _cancelToken = CancelToken(); + + _cancelToken.cancel(); + } + rethrow; + } } Future _uploadStreamWithFetchClient({ @@ -101,11 +109,6 @@ class TurboUploadServiceImpl implements TurboUploadService { int dataItemSize = 0; - // TODO: remove after fixing the issue with the size of the upload - await for (final data in dataItem.streamGenerator()) { - dataItemSize += data.length; - } - StreamTransformer createPassthroughTransformer() { return StreamTransformer.fromHandlers( handleData: (Uint8List data, EventSink sink) { @@ -120,23 +123,15 @@ class TurboUploadServiceImpl implements TurboUploadService { ); } - final client = FetchClient( - mode: RequestMode.cors, - streamRequests: true, - cache: RequestCache.noCache, - ); - - final controller = StreamController>(sync: false); - final request = ArDriveStreamedRequest( 'POST', Uri.parse(url), - controller, + _fetchController, )..headers.addAll({ 'content-type': 'application/octet-stream', }); - controller + _fetchController .addStream( dataItem.streamGenerator().transform( createPassthroughTransformer(), @@ -147,27 +142,41 @@ class TurboUploadServiceImpl implements TurboUploadService { request.sink.close(); }); - controller.onPause = () { + _fetchController.onPause = () { print('Paused'); }; - controller.onResume = () { + _fetchController.onResume = () { print('Resumed'); }; - request.contentLength = dataItemSize; + try { + request.contentLength = dataItemSize; + request.persistentConnection = false; + + print('is persistent connection?${request.persistentConnection}'); - final response = await client.send(request); + final response = await client.send(request); - print(await utf8.decodeStream(response.stream)); + print(await utf8.decodeStream(response.stream)); - return response; + return response; + } catch (e) { + print('Error on turbo upload using FetchClient: $e'); + rethrow; + } } @override - bool get isPossibleGetProgress => _isPossibleGetProgress; + Future cancel() async { + _cancelToken.cancel(); + client.close(); + _fetchController.close(); + _isCanceled = true; + print('Stream closed'); + } - bool _isPossibleGetProgress = false; + bool _isCanceled = false; } class TurboUploadExceptions implements Exception {} diff --git a/packages/ardrive_uploader/lib/src/upload_controller.dart b/packages/ardrive_uploader/lib/src/upload_controller.dart index 4b0cb6b77b..ac5c9cb5d0 100644 --- a/packages/ardrive_uploader/lib/src/upload_controller.dart +++ b/packages/ardrive_uploader/lib/src/upload_controller.dart @@ -1,7 +1,10 @@ import 'dart:async'; +import 'package:ardrive_io/ardrive_io.dart'; +import 'package:ardrive_uploader/src/data_bundler.dart'; import 'package:ardrive_uploader/src/streamed_upload.dart'; import 'package:arweave/arweave.dart'; +import 'package:cryptography/cryptography.dart'; import 'package:rxdart/rxdart.dart'; import 'package:uuid/uuid.dart'; @@ -14,23 +17,160 @@ abstract class UploadItem { UploadItem({required this.size, required this.data}); } -class DataItemUploadTask extends UploadItem { - DataItemUploadTask({required int size, required DataItemResult data}) +class BundleDataItemUploadItem extends UploadItem { + BundleDataItemUploadItem({required int size, required DataItemResult data}) : super(size: size, data: data); } -class TransactionUploadTask extends UploadItem { - TransactionUploadTask({required int size, required TransactionResult data}) +class BundleTransactionUploadItem extends UploadItem { + BundleTransactionUploadItem( + {required int size, required TransactionResult data}) : super(size: size, data: data); } -abstract class _UploadTask { +class FolderUploadTask implements UploadTask { + final List<(ARFSFolderUploadMetatadata, IOEntity)> folders; + + @override + final UploadItem? uploadItem; + + @override + final StreamedUpload streamedUpload; + + @override + final List? content; + + @override + final double progress; + + @override + final String id; + + @override + bool isProgressAvailable = true; + + FolderUploadTask({ + required this.folders, + this.uploadItem, + this.isProgressAvailable = true, + this.status = UploadStatus.notStarted, + this.content, + this.encryptionKey, + required this.streamedUpload, + this.progress = 0, + String? id, + }) : id = id ?? const Uuid().v4(); + + @override + UploadStatus status; + + @override + FolderUploadTask copyWith({ + UploadItem? uploadItem, + double? progress, + bool? isProgressAvailable, + UploadStatus? status, + String? id, + List? content, + SecretKey? encryptionKey, + List<(ARFSFolderUploadMetatadata, IOEntity)>? folders, + StreamedUpload? streamedUpload, + }) { + return FolderUploadTask( + streamedUpload: streamedUpload ?? this.streamedUpload, + folders: folders ?? this.folders, + uploadItem: uploadItem ?? this.uploadItem, + content: content ?? this.content, + id: id ?? this.id, + progress: progress ?? this.progress, + isProgressAvailable: isProgressAvailable ?? this.isProgressAvailable, + status: status ?? this.status, + ); + } + + @override + final SecretKey? encryptionKey; +} + +class FileUploadTask extends UploadTask { + final IOFile file; + + final ARFSFileUploadMetadata metadata; + + @override + final StreamedUpload streamedUpload; + + @override + final UploadItem? uploadItem; + + @override + final List? content; + + @override + final double progress; + + @override + final String id; + + @override + bool isProgressAvailable = true; + + FileUploadTask({ + this.uploadItem, + this.isProgressAvailable = true, + this.status = UploadStatus.notStarted, + this.content, + String? id, + required this.file, + required this.metadata, + this.encryptionKey, + required this.streamedUpload, + this.progress = 0, + }) : id = id ?? const Uuid().v4(); + + @override + UploadStatus status; + + @override + FileUploadTask copyWith({ + UploadItem? uploadItem, + double? progress, + bool? isProgressAvailable, + UploadStatus? status, + String? id, + ARFSFileUploadMetadata? metadata, + List? content, + SecretKey? encryptionKey, + StreamedUpload? streamedUpload, + }) { + print('Copying new task with status: ${status ?? this.status}'); + return FileUploadTask( + streamedUpload: streamedUpload ?? this.streamedUpload, + encryptionKey: encryptionKey ?? this.encryptionKey, + metadata: metadata ?? this.metadata, + uploadItem: uploadItem ?? this.uploadItem, + content: content ?? this.content, + id: id ?? this.id, + isProgressAvailable: isProgressAvailable ?? this.isProgressAvailable, + status: status ?? this.status, + file: file, + progress: progress ?? this.progress, + ); + } + + @override + final SecretKey? encryptionKey; +} + +abstract class UploadTask { abstract final String id; abstract final UploadItem? uploadItem; abstract final List? content; - abstract double progress; - abstract bool isProgressAvailable; - abstract UploadStatus status; + abstract final double progress; + abstract final bool isProgressAvailable; + abstract final UploadStatus status; + abstract final SecretKey? encryptionKey; + abstract final StreamedUpload streamedUpload; UploadTask copyWith({ UploadItem? uploadItem, @@ -42,7 +182,7 @@ abstract class _UploadTask { }); } -class UploadTask implements _UploadTask { +class ARFSUploadTask implements UploadTask { @override final UploadItem? uploadItem; @@ -58,11 +198,12 @@ class UploadTask implements _UploadTask { @override bool isProgressAvailable = true; - UploadTask({ + ARFSUploadTask({ this.uploadItem, this.isProgressAvailable = true, this.status = UploadStatus.notStarted, this.content, + this.encryptionKey, String? id, }) : id = id ?? const Uuid().v4(); @@ -70,7 +211,7 @@ class UploadTask implements _UploadTask { UploadStatus status; @override - UploadTask copyWith({ + ARFSUploadTask copyWith({ UploadItem? uploadItem, double? progress, bool? isProgressAvailable, @@ -78,7 +219,7 @@ class UploadTask implements _UploadTask { String? id, List? content, }) { - return UploadTask( + return ARFSUploadTask( uploadItem: uploadItem ?? this.uploadItem, content: content ?? this.content, id: id ?? this.id, @@ -86,31 +227,44 @@ class UploadTask implements _UploadTask { status: status ?? this.status, ); } + + @override + final SecretKey? encryptionKey; + + @override + // TODO: implement streamedUpload + StreamedUpload get streamedUpload => throw UnimplementedError(); } -// TODO: Review this file abstract class UploadController { abstract final Map tasks; - /// TODO: implement the sendTasks method - Future sendTasks(); - Future retryTask(UploadTask task, Wallet wallet); - Future retryFailedTasks(Wallet wallet); Future close(); - void cancel(); - void onCancel(); + Future cancel(); + void onCancel(Function(List tasks) callback); void onDone(Function(List tasks) callback); void onError(Function(List tasks) callback); void updateProgress({UploadTask? task}); void onProgressChange(Function(UploadProgress progress) callback); + void sendTasks( + Wallet wallet, + ); + void sendTask(UploadTask task, Wallet wallet, {Function()? onTaskCompleted}); + void addTask(UploadTask task); factory UploadController( StreamController progressStream, StreamedUpload streamedUpload, - ) { + DataBundler dataBundler, { + int numOfWorkers = 5, + int maxTasksPerWorker = 5, + }) { return _UploadController( progressStream: progressStream, streamedUpload: streamedUpload, + dataBundler: dataBundler, + numOfWorkers: numOfWorkers, + maxTasksPerWorker: maxTasksPerWorker, ); } } @@ -118,11 +272,20 @@ abstract class UploadController { class _UploadController implements UploadController { final StreamController _progressStream; final StreamedUpload _streamedUpload; + final DataBundler _dataBundler; + final int _numOfWorkers; + final int _maxTasksPerWorker; _UploadController({ required StreamController progressStream, required StreamedUpload streamedUpload, - }) : _progressStream = progressStream, + required DataBundler dataBundler, + int numOfWorkers = 5, + int maxTasksPerWorker = 5, + }) : _dataBundler = dataBundler, + _numOfWorkers = numOfWorkers, + _maxTasksPerWorker = maxTasksPerWorker, + _progressStream = progressStream, _streamedUpload = streamedUpload { init(); } @@ -131,6 +294,8 @@ class _UploadController implements UploadController { bool get isCanceled => _isCanceled; DateTime? _start; + WorkerPool? workerPool; + void init() { _isCanceled = false; late StreamSubscription subscription; @@ -138,20 +303,24 @@ class _UploadController implements UploadController { subscription = _progressStream.stream.debounceTime(Duration(milliseconds: 100)).listen( (event) async { + if (_isCanceled) { + return; + } + _start ??= DateTime.now(); _onProgressChange!(event); - if (_uploadProgress.progress == 1) { + final finishedTasksLength = + _failedTasks.length + _completedTasks.length; + + if (finishedTasksLength == tasks.length) { await close(); return; } }, onDone: () { print('Done upload'); - for (var task in tasks.values) { - print('Task: ${task.id} - ${task.status}'); - } _onDone(tasks.values.toList()); subscription.cancel(); }, @@ -168,15 +337,36 @@ class _UploadController implements UploadController { } @override - void cancel() { - // TODO: it's uploading closing the progress stream. We need to cancel the upload + Future cancel() async { + workerPool?.cancel(); _isCanceled = true; + + final cancelableTask = tasks.values + .where((e) => + e.status != UploadStatus.complete && + e.status != UploadStatus.failed) + .toList(); + + final cancelTasksFuture = cancelableTask.map((task) async { + await task.streamedUpload.cancel(task, this); + + task = task.copyWith(status: UploadStatus.canceled); + + _canceledTasks.putIfAbsent(task.id, () => task); + + updateProgress(task: task); + }); + + await Future.wait(cancelTasksFuture); + + _onCancel(_canceledTasks.values.toList()); + _progressStream.close(); } @override - void onCancel() { - // TODO: implement onCancel + void onCancel(Function(List tasks) callback) { + _onCancel = callback; } @override @@ -193,6 +383,12 @@ class _UploadController implements UploadController { } if (task != null) { + if (task.status == UploadStatus.complete) { + _completedTasks[task.id] = task; + } else if (task.status == UploadStatus.failed) { + _failedTasks[task.id] = task; + } + tasks[task.id] = task; // TODO: Check how to improve this @@ -201,15 +397,15 @@ class _UploadController implements UploadController { // TODO: Check how to improve this _uploadProgress = _uploadProgress.copyWith( task: taskList, - progress: calculateTotalProgress(taskList), + progressInPercentage: calculateTotalProgress(taskList), totalSize: totalSize(taskList), totalUploaded: totalUploaded(taskList), startTime: _start, ); - - _progressStream.add(_uploadProgress); } + _progressStream.add(_uploadProgress); + return; } @@ -229,8 +425,15 @@ class _UploadController implements UploadController { print('Upload Finished'); }; + void Function(List tasks) _onCancel = (List tasks) { + print('Upload Canceled'); + }; + @override final Map tasks = {}; + final Map _completedTasks = {}; + final Map _failedTasks = {}; + final Map _canceledTasks = {}; // TODO: CALCULATE BASED ON TOTAL SIZE NOT ONLY ON THE NUMBER OF TASKS double calculateTotalProgress(List tasks) { @@ -258,18 +461,18 @@ class _UploadController implements UploadController { for (var task in tasks) { if (task.uploadItem != null) { totalSize += task.uploadItem!.size; + } else { + if (task is FileUploadTask) { + totalSize += task.metadata.size; + } } } return totalSize; } - @override - Future sendTasks() async { - // TODO: implement sendTasks - } - - @override + /// It is just an experimentation. It is not used yet, but it will be used in the future. + /// When this implementation is stable, we must add this method on its interface class: `UploadController`. Future retryFailedTasks(Wallet wallet) async { final failedTasks = tasks.values.where((e) => e.status == UploadStatus.failed).toList(); @@ -287,7 +490,8 @@ class _UploadController implements UploadController { } } - @override + /// It is just an experimentation. It is not used yet, but it will be used in the future. + /// When this implementation is stable, we must add this method on its interface class: `UploadController`. Future retryTask(UploadTask task, Wallet wallet) async { task.copyWith(status: UploadStatus.notStarted); @@ -295,6 +499,45 @@ class _UploadController implements UploadController { _streamedUpload.send(task, wallet, this); } + + @override + void sendTasks(Wallet wallet) { + if (tasks.isEmpty) { + throw Exception('No tasks to send'); + } + + // creates a worker pool and initializes it with the tasks + workerPool = WorkerPool( + numWorkers: _numOfWorkers, + maxTasksPerWorker: _maxTasksPerWorker, + taskQueue: tasks.values + .where((element) => element.status == UploadStatus.notStarted) + .toList(), + wallet: wallet, + dataBundler: _dataBundler, + uploadController: this, + onTaskCompleted: (task) { + updateProgress(task: task); + }, + ); + } + + @override + void addTask(UploadTask task) { + tasks[task.id] = task; + } + + @override + void sendTask(UploadTask task, Wallet wallet, {Function()? onTaskCompleted}) { + Worker( + wallet: wallet, + dataBundler: _dataBundler, + uploadController: this, + onTaskCompleted: () { + onTaskCompleted?.call(); + }, + ).addTask(task); + } } enum UploadStatus { @@ -304,24 +547,34 @@ enum UploadStatus { /// The upload is in progress inProgress, + /// The upload is being prepared + creatingMetadata, + + /// The upload is being bundled + creatingBundle, + + /// The upload is being encrypted + encryting, + /// The upload is paused paused, - bundling, - + /// The upload is prepartion is done: the bundle is ready to be uploaded preparationDone, - encryting, - /// The upload is complete complete, /// The upload has failed failed, + + /// The upload has been canceled + canceled, } class UploadProgress { - final double progress; + /// The progress in percentage from 0 to 1 + final double progressInPercentage; final int totalSize; final int totalUploaded; final List task; @@ -329,7 +582,7 @@ class UploadProgress { DateTime? startTime; UploadProgress({ - required this.progress, + required this.progressInPercentage, required this.totalSize, required this.task, required this.totalUploaded, @@ -338,7 +591,7 @@ class UploadProgress { factory UploadProgress.notStarted() { return UploadProgress( - progress: 0, + progressInPercentage: 0, totalSize: 0, task: [], totalUploaded: 0, @@ -346,7 +599,7 @@ class UploadProgress { } UploadProgress copyWith({ - double? progress, + double? progressInPercentage, int? totalSize, List? task, int? totalUploaded, @@ -354,7 +607,7 @@ class UploadProgress { }) { return UploadProgress( startTime: startTime ?? this.startTime, - progress: progress ?? this.progress, + progressInPercentage: progressInPercentage ?? this.progressInPercentage, totalSize: totalSize ?? this.totalSize, task: task ?? this.task, totalUploaded: totalUploaded ?? this.totalUploaded, @@ -411,3 +664,210 @@ class UploadProgress { return (totalUploaded / elapsedTime).toDouble(); // Assuming speed in MB/s } } + +class Worker { + final Function() onTaskCompleted; + final int maxTasks; + final DataBundler dataBundler; + final Wallet wallet; + final UploadController uploadController; + + List> taskFutures = []; + + Worker({ + required this.onTaskCompleted, + this.maxTasks = 5, + required this.dataBundler, + required this.wallet, + required this.uploadController, + }); + + void addTask(UploadTask task) { + if (taskFutures.length < maxTasks) { + final future = _performUpload(task); + taskFutures.add(future); + + future.then((_) { + taskFutures.remove(future); + onTaskCompleted(); + }); + } + } + + Future _performUpload(UploadTask task) async { + // Your upload logic here + /// Can be either a DataItemResult or a TransactionResult + dynamic bundle; + + try { + if (task is FileUploadTask) { + task = task.copyWith(content: [task.metadata]); + + bundle = await dataBundler.createDataBundle( + file: task.file, + metadata: task.metadata, + wallet: wallet, + driveKey: task.encryptionKey, + onStartBundleCreation: () { + print('Creating bundle'); + task = task.copyWith( + status: UploadStatus.creatingBundle, + ); + + uploadController.updateProgress( + task: task, + ); + }, + onStartMetadataCreation: () { + print('Creating metadata'); + task = task.copyWith( + status: UploadStatus.creatingMetadata, + ); + + uploadController.updateProgress( + task: task, + ); + }, + ); + } else if (task is FolderUploadTask) { + // creates the bundle for folders + bundle = await dataBundler.createDataBundleForEntities( + entities: task.folders, + wallet: wallet, + driveKey: task.encryptionKey, + ); + + final folderBundle = (bundle as List).first; + + bundle = folderBundle.dataItemResult; + } + + /// The upload can be canceled while the bundle is being created + if (task.status == UploadStatus.canceled) { + print('Upload canceled while bundle was being created'); + return; + } + + if (bundle is TransactionResult) { + task = task.copyWith( + uploadItem: BundleTransactionUploadItem( + size: bundle.dataSize, + data: bundle, + ), + ); + } else if (bundle is DataItemResult) { + task = task.copyWith( + uploadItem: BundleDataItemUploadItem( + size: bundle.dataItemSize, + data: bundle, + ), + ); + } else { + throw Exception('Unknown bundle type'); + } + + uploadController.updateProgress( + task: task, + ); + + if (_isCanceled) { + print('Upload canceled after bundle creation and before upload'); + return; + } + + final value = + await task.streamedUpload.send(task, wallet, uploadController); + + return value; + } catch (e) { + /// Adds the status failed to the upload task and stops the upload. + task = task.copyWith( + status: UploadStatus.failed, + ); + uploadController.updateProgress( + task: task, + ); + print('Error: $e'); + } + } + + void cancel() { + _isCanceled = true; + } + + bool _isCanceled = false; +} + +class WorkerPool { + final int numWorkers; + final int maxTasksPerWorker; + final List taskQueue; + final List workers; + final Wallet wallet; + final DataBundler dataBundler; + final UploadController uploadController; + + WorkerPool({ + required this.numWorkers, + required this.maxTasksPerWorker, + required this.taskQueue, + required this.wallet, + required Function(UploadTask task) onTaskCompleted, + required this.dataBundler, + required this.uploadController, + }) : workers = List.generate( + numWorkers, + (index) => Worker( + wallet: wallet, + dataBundler: dataBundler, + uploadController: uploadController, + onTaskCompleted: () {}, + ), + ) { + _setWorkerCallbacks(); + _initializeWorkers(); + } + + void _setWorkerCallbacks() { + for (var i = 0; i < numWorkers; i++) { + workers[i] = Worker( + wallet: wallet, + dataBundler: dataBundler, + uploadController: uploadController, + onTaskCompleted: () { + if (_isCanceled) { + return; + } + + _assignNextTask(i); + }, + ); + } + } + + void _initializeWorkers() { + for (var i = 0; i < numWorkers; i++) { + for (var j = 0; j < maxTasksPerWorker; j++) { + _assignNextTask(i); + } + } + } + + void _assignNextTask(int workerIndex) { + if (taskQueue.isNotEmpty) { + final nextTask = taskQueue.removeAt(0); + workers[workerIndex].addTask(nextTask); + } + } + + void cancel() { + _isCanceled = true; + for (var element in workers) { + element.cancel(); + } + } + + bool get isCanceled => _isCanceled; + + bool _isCanceled = false; +} diff --git a/packages/ardrive_uploader/pubspec.yaml b/packages/ardrive_uploader/pubspec.yaml index bba70e5666..ca7563f9f9 100644 --- a/packages/ardrive_uploader/pubspec.yaml +++ b/packages/ardrive_uploader/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: arweave: git: url: https://github.com/ardriveapp/arweave-dart.git - ref: PE-3697 + ref: PE-4812-cancel-button-fails-to-stop-file-upload-when-using-a-rs-only ardrive_utils: path: ../ardrive_utils ardrive_crypto: @@ -29,6 +29,8 @@ dependencies: path: ../arconnect arfs: path: ../arfs + pst: + path: ../pst json_annotation: ^4.8.0 uuid: ^3.0.4 system_info_plus: ^0.0.5 diff --git a/packages/ardrive_utils/lib/ardrive_utils.dart b/packages/ardrive_utils/lib/ardrive_utils.dart index 25dbec17a5..281594bd78 100644 --- a/packages/ardrive_utils/lib/ardrive_utils.dart +++ b/packages/ardrive_utils/lib/ardrive_utils.dart @@ -2,6 +2,14 @@ library ardrive_utils; export 'src/app_info_services.dart'; export 'src/app_platform.dart'; +export 'src/base2_size.dart'; +export 'src/convert_to_usd.dart'; export 'src/entity_tag.dart'; +export 'src/get_first_future_result.dart'; export 'src/html/html.dart'; export 'src/sign_nounce_and_data.dart'; +export 'src/streams.dart'; +export 'src/types/arweave_address.dart'; +export 'src/types/string_types.dart'; +export 'src/types/transaction_id.dart'; +export 'src/types/winston.dart'; diff --git a/packages/ardrive_utils/lib/src/app_info_services.dart b/packages/ardrive_utils/lib/src/app_info_services.dart index a8c6a12df4..44067e5005 100644 --- a/packages/ardrive_utils/lib/src/app_info_services.dart +++ b/packages/ardrive_utils/lib/src/app_info_services.dart @@ -48,4 +48,4 @@ class AppInfoServices { } const String appName = 'ArDrive-App'; -const String arfsVersion = '0.12'; +const String arfsVersion = '0.13'; diff --git a/packages/ardrive_utils/lib/src/app_platform.dart b/packages/ardrive_utils/lib/src/app_platform.dart index 6cd488c64e..cc35170c05 100644 --- a/packages/ardrive_utils/lib/src/app_platform.dart +++ b/packages/ardrive_utils/lib/src/app_platform.dart @@ -50,6 +50,16 @@ class AppPlatform { final info = await (deviceInfo ?? DeviceInfoPlugin()).deviceInfo; return info is WebBrowserInfo && info.browserName == BrowserName.firefox; } + + static Future isChrome({DeviceInfoPlugin? deviceInfo}) async { + final info = await (deviceInfo ?? DeviceInfoPlugin()).deviceInfo; + return info is WebBrowserInfo && info.browserName == BrowserName.chrome; + } + + static Future isSafari({DeviceInfoPlugin? deviceInfo}) async { + final info = await (deviceInfo ?? DeviceInfoPlugin()).deviceInfo; + return info is WebBrowserInfo && info.browserName == BrowserName.safari; + } } // ignore: constant_identifier_names diff --git a/lib/utils/data_size.dart b/packages/ardrive_utils/lib/src/base2_size.dart similarity index 100% rename from lib/utils/data_size.dart rename to packages/ardrive_utils/lib/src/base2_size.dart diff --git a/packages/ardrive_utils/lib/src/convert_to_usd.dart b/packages/ardrive_utils/lib/src/convert_to_usd.dart new file mode 100644 index 0000000000..de236a0178 --- /dev/null +++ b/packages/ardrive_utils/lib/src/convert_to_usd.dart @@ -0,0 +1,18 @@ +import 'package:ardrive_http/ardrive_http.dart'; + +Future getArUsdConversionRateOrNull() async { + try { + return await getArUsdConversionRate(); + } catch (e) { + return null; + } +} + +Future getArUsdConversionRate() async { + const String coinGeckoApi = + 'https://api.coingecko.com/api/v3/simple/price?ids=arweave&vs_currencies=usd'; + + final response = await ArDriveHTTP(retries: 3).getJson(coinGeckoApi); + + return response.data?['arweave']['usd']; +} diff --git a/packages/ardrive_utils/lib/src/get_first_future_result.dart b/packages/ardrive_utils/lib/src/get_first_future_result.dart new file mode 100644 index 0000000000..711684ecb7 --- /dev/null +++ b/packages/ardrive_utils/lib/src/get_first_future_result.dart @@ -0,0 +1,29 @@ +import 'dart:async'; + +class FutureError { + Object error; + StackTrace stackTrace; + FutureError(this.error, this.stackTrace); +} + +Future getFirstFutureResult(Iterable> futures) { + final completer = Completer.sync(); + final errors = []; + + void onValue(T value) { + if (!completer.isCompleted) completer.complete(value); + } + + void onError(Object error, StackTrace stack) { + errors.add(FutureError(error, stack)); + if (!completer.isCompleted && errors.length == futures.length) { + completer.completeError(errors); + } + } + + for (var future in futures) { + future.then(onValue, onError: onError); + } + + return completer.future; +} diff --git a/packages/ardrive_utils/lib/src/streams.dart b/packages/ardrive_utils/lib/src/streams.dart new file mode 100644 index 0000000000..3d5404a881 --- /dev/null +++ b/packages/ardrive_utils/lib/src/streams.dart @@ -0,0 +1,17 @@ +import 'dart:typed_data'; + +Future streamToUint8List(Stream stream) async { + List collectedData = await stream.toList(); + int totalLength = + collectedData.fold(0, (prev, element) => prev + element.length); + + final result = Uint8List(totalLength); + int offset = 0; + + for (var data in collectedData) { + result.setRange(offset, offset + data.length, data); + offset += data.length; + } + + return result; +} diff --git a/lib/types/arweave_address.dart b/packages/ardrive_utils/lib/src/types/arweave_address.dart similarity index 100% rename from lib/types/arweave_address.dart rename to packages/ardrive_utils/lib/src/types/arweave_address.dart diff --git a/lib/entities/string_types.dart b/packages/ardrive_utils/lib/src/types/string_types.dart similarity index 76% rename from lib/entities/string_types.dart rename to packages/ardrive_utils/lib/src/types/string_types.dart index 37b2b18541..0cd2aef2bb 100644 --- a/lib/entities/string_types.dart +++ b/packages/ardrive_utils/lib/src/types/string_types.dart @@ -3,5 +3,5 @@ typedef FileID = String; typedef DriveID = String; typedef TxID = String; -typedef ArweaveAddress = String; +typedef ArweaveAddressString = String; typedef Privacy = String; diff --git a/lib/types/transaction_id.dart b/packages/ardrive_utils/lib/src/types/transaction_id.dart similarity index 100% rename from lib/types/transaction_id.dart rename to packages/ardrive_utils/lib/src/types/transaction_id.dart diff --git a/lib/types/winston.dart b/packages/ardrive_utils/lib/src/types/winston.dart similarity index 100% rename from lib/types/winston.dart rename to packages/ardrive_utils/lib/src/types/winston.dart diff --git a/packages/ardrive_utils/pubspec.yaml b/packages/ardrive_utils/pubspec.yaml index 97fdcb2827..cbb3bc6ae1 100644 --- a/packages/ardrive_utils/pubspec.yaml +++ b/packages/ardrive_utils/pubspec.yaml @@ -15,11 +15,16 @@ dependencies: package_info_plus: ^4.1.0 platform: ^3.1.0 universal_html: ^2.2.4 + ardrive_http: + git: + url: https://github.com/ar-io/ardrive_http.git + ref: v1.3.1 arweave: git: url: https://github.com/ardriveapp/arweave-dart.git ref: PE-3697 js: ^0.6.7 + equatable: ^2.0.5 dev_dependencies: flutter_test: diff --git a/test/utils/data_size_test.dart b/packages/ardrive_utils/test/base2_size.dart similarity index 97% rename from test/utils/data_size_test.dart rename to packages/ardrive_utils/test/base2_size.dart index 98c78ade75..b79159f7c7 100644 --- a/test/utils/data_size_test.dart +++ b/packages/ardrive_utils/test/base2_size.dart @@ -1,4 +1,4 @@ -import 'package:ardrive/utils/data_size.dart'; +import 'package:ardrive_utils/src/base2_size.dart'; import 'package:flutter_test/flutter_test.dart'; /// https://www.kylesconverter.com/data-storage/gibibits-to-bits diff --git a/packages/ardrive_utils/test/src/ardrive_utils_test.dart b/packages/ardrive_utils/test/src/ardrive_utils_test.dart new file mode 100644 index 0000000000..ab73b3a234 --- /dev/null +++ b/packages/ardrive_utils/test/src/ardrive_utils_test.dart @@ -0,0 +1 @@ +void main() {} diff --git a/packages/ardrive_utils/test/src/base2_size.dart b/packages/ardrive_utils/test/src/base2_size.dart new file mode 100644 index 0000000000..b79159f7c7 --- /dev/null +++ b/packages/ardrive_utils/test/src/base2_size.dart @@ -0,0 +1,75 @@ +import 'package:ardrive_utils/src/base2_size.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// https://www.kylesconverter.com/data-storage/gibibits-to-bits +void main() { + group('Testing KiB class', () { + test('should return correct size', () { + expect(const KiB(1).size, 1024); + }); + + test('should return correct size', () { + expect(const KiB(150).size, 153600); + }); + + test('should return correct size', () { + expect(const KiB(15).size, 15360); + }); + + test('should return correct size', () { + expect(const KiB(25).size, 25600); + }); + + test('should return correct size', () { + expect(const KiB(0).size, 0); + }); + }); + + group('Testing MiB class', () { + test('should return correct size', () { + expect(const MiB(200).size, 209715200); + }); + + test('should return correct size', () { + expect(const MiB(480).size, 503316480); + }); + + test('should return correct size', () { + expect(const KiB(15).size, 15360); + }); + + test('should return correct size', () { + expect(const MiB(1).size, const KiB(1024).size); + }); + + test('should return correct size', () { + expect(const MiB(900).size, 943718400); + }); + + test('should return correct size', () { + expect(const MiB(0).size, 0); + }); + }); + + group('Testing GiB class', () { + test('should return correct size', () { + expect(const GiB(1).size, 1073741824); + }); + + test('should return correct size', () { + expect(const GiB(15).size, 16106127360); + }); + + test('should return correct size', () { + expect(const GiB(25).size, 26843545600); + }); + + test('should return correct size', () { + expect(const GiB(1).size, const MiB(1024).size); + }); + + test('should return correct size', () { + expect(const GiB(0).size, 0); + }); + }); +} diff --git a/packages/ardrive_utils/test/src/get_first_future_result.dart b/packages/ardrive_utils/test/src/get_first_future_result.dart new file mode 100644 index 0000000000..ba55427d15 --- /dev/null +++ b/packages/ardrive_utils/test/src/get_first_future_result.dart @@ -0,0 +1,48 @@ +import 'package:ardrive_utils/src/get_first_future_result.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('should return the first future completed with a value', () async { + final future1sec = + Future.delayed(const Duration(seconds: 1)).then((value) => 1); + final future2sec = + Future.delayed(const Duration(seconds: 2)).then((value) => 2); + final future3sec = + Future.delayed(const Duration(seconds: 3)).then((value) => 3); + + final value = + await getFirstFutureResult([future1sec, future2sec, future3sec]); + + expect(value, 1); + }); + + test( + 'should return the first future completed with a value even if some future failed before', + () async { + final future1sec = Future.delayed(const Duration(seconds: 1)) + .then((value) => throw Exception()); + final future2sec = + Future.delayed(const Duration(seconds: 2)).then((value) => 2); + final future3sec = + Future.delayed(const Duration(seconds: 3)).then((value) => 3); + + final value = + await getFirstFutureResult([future1sec, future2sec, future3sec]); + + expect(value, 2); + }); + + test('should throws only when ALL fails', () async { + final future1sec = Future.delayed(const Duration(seconds: 1)) + .then((value) => throw Exception()); + final future2sec = Future.delayed(const Duration(seconds: 2)) + .then((value) => throw Exception()); + final future3sec = Future.delayed(const Duration(seconds: 3)) + .then((value) => throw Exception()); + + expectLater( + () async => + await getFirstFutureResult([future1sec, future2sec, future3sec]), + throwsA(const TypeMatcher())); + }); +} diff --git a/test/types/ardrive_address_test.dart b/packages/ardrive_utils/test/src/types/ardrive_address_test.dart similarity index 88% rename from test/types/ardrive_address_test.dart rename to packages/ardrive_utils/test/src/types/ardrive_address_test.dart index 68f36ed96f..135cd8b3e6 100644 --- a/test/types/ardrive_address_test.dart +++ b/packages/ardrive_utils/test/src/types/ardrive_address_test.dart @@ -1,5 +1,5 @@ -import 'package:ardrive/types/arweave_address.dart'; -import 'package:test/test.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { group('ArweaveAddress type', () { diff --git a/test/types/transaction_id_test.dart b/packages/ardrive_utils/test/src/types/transaction_id_test.dart similarity index 88% rename from test/types/transaction_id_test.dart rename to packages/ardrive_utils/test/src/types/transaction_id_test.dart index d474a6930d..39b1e75e78 100644 --- a/test/types/transaction_id_test.dart +++ b/packages/ardrive_utils/test/src/types/transaction_id_test.dart @@ -1,5 +1,5 @@ -import 'package:ardrive/types/transaction_id.dart'; -import 'package:test/test.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { group('TransactionID type', () { diff --git a/test/types/winston_test.dart b/packages/ardrive_utils/test/src/types/winston_test.dart similarity index 96% rename from test/types/winston_test.dart rename to packages/ardrive_utils/test/src/types/winston_test.dart index 1bfd574530..7dd563fe17 100644 --- a/test/types/winston_test.dart +++ b/packages/ardrive_utils/test/src/types/winston_test.dart @@ -1,5 +1,5 @@ -import 'package:ardrive/types/winston.dart'; -import 'package:test/test.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { group('Winston type', () { diff --git a/packages/pst/.gitignore b/packages/pst/.gitignore new file mode 100644 index 0000000000..96486fd930 --- /dev/null +++ b/packages/pst/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/pst/.metadata b/packages/pst/.metadata new file mode 100644 index 0000000000..eea17bc4a0 --- /dev/null +++ b/packages/pst/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "2f708eb8396e362e280fac22cf171c2cb467343c" + channel: "stable" + +project_type: package diff --git a/packages/pst/CHANGELOG.md b/packages/pst/CHANGELOG.md new file mode 100644 index 0000000000..41cc7d8192 --- /dev/null +++ b/packages/pst/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/pst/LICENSE b/packages/pst/LICENSE new file mode 100644 index 0000000000..ba75c69f7f --- /dev/null +++ b/packages/pst/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/pst/README.md b/packages/pst/README.md new file mode 100644 index 0000000000..02fe8ecabc --- /dev/null +++ b/packages/pst/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/pst/analysis_options.yaml b/packages/pst/analysis_options.yaml new file mode 100644 index 0000000000..a5744c1cfb --- /dev/null +++ b/packages/pst/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/pst/lib/pst.dart b/packages/pst/lib/pst.dart new file mode 100644 index 0000000000..ee4508883f --- /dev/null +++ b/packages/pst/lib/pst.dart @@ -0,0 +1,13 @@ +library pst; + +export 'src/ardrive_contract_oracle.dart'; +export 'src/community_oracle.dart'; +export 'src/contract_oracle.dart'; +export 'src/contract_reader.dart'; +export 'src/contract_readers/arns_contract_reader.dart'; +export 'src/contract_readers/redstone_contract_reader.dart'; +export 'src/contract_readers/smartweave_contract_reader.dart'; +export 'src/contract_readers/verto_contract_reader.dart'; +export 'src/pst.dart'; +export 'src/pst_contract_data.dart'; +export 'src/pst_contract_data_builder.dart'; diff --git a/lib/pst/ardrive_contract_oracle.dart b/packages/pst/lib/src/ardrive_contract_oracle.dart similarity index 63% rename from lib/pst/ardrive_contract_oracle.dart rename to packages/pst/lib/src/ardrive_contract_oracle.dart index ecb1d40e6a..8fd0f8c9e3 100644 --- a/lib/pst/ardrive_contract_oracle.dart +++ b/packages/pst/lib/src/ardrive_contract_oracle.dart @@ -1,21 +1,21 @@ -import 'package:ardrive/pst/contract_oracle.dart'; -import 'package:ardrive/pst/pst_contract_data.dart'; -import 'package:ardrive/utils/get_first_future_result.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:pst/src/contract_oracle.dart'; +import 'package:pst/src/pst_contract_data.dart'; import 'package:retry/retry.dart'; const _maxReadContractAttempts = 3; class ArDriveContractOracle implements ContractOracle { final List _contractOracles; + final ContractOracle? _fallbackContractOracle; - ArDriveContractOracle(List contractOracles) - : _contractOracles = contractOracles { - if (contractOracles.isEmpty) { - throw const EmptyContractOracles(); - } - } - + ArDriveContractOracle( + List contractOracles, { + ContractOracle? fallbackContractOracle, + }) : _fallbackContractOracle = fallbackContractOracle, + _contractOracles = contractOracles; @override Future getCommunityContract() async { try { @@ -31,12 +31,27 @@ class ArDriveContractOracle implements ContractOracle { /// iterates over all contract readers attempting to read the contract Future _getContractFromOracles() async { - final contract = await getFirstFutureResult( - _contractOracles - .map((e) async => await _getContractWithRetries(e)) - .toList()); + try { + if (_contractOracles.isEmpty) { + throw const EmptyContractOracles(); + } - return contract; + final contract = await getFirstFutureResult( + _contractOracles + .map((e) async => await _getContractWithRetries(e)) + .toList()); + + return contract; + } catch (e) { + debugPrint('Could not read contract state from any of the oracles'); + if (_fallbackContractOracle == null) { + throw const CouldNotReadContractState( + reason: 'No fallback contract reader provided', + ); + } + + return _getContractWithRetries(_fallbackContractOracle!); + } } /// attempts multiple retries to read the given contract oracle diff --git a/lib/pst/community_oracle.dart b/packages/pst/lib/src/community_oracle.dart similarity index 75% rename from lib/pst/community_oracle.dart rename to packages/pst/lib/src/community_oracle.dart index 72e8dc98d6..d533b17380 100644 --- a/lib/pst/community_oracle.dart +++ b/packages/pst/lib/src/community_oracle.dart @@ -1,24 +1,29 @@ import 'dart:math'; -import 'package:ardrive/pst/contract_oracle.dart'; -import 'package:ardrive/pst/utils.dart'; -import 'package:ardrive/types/winston.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:equatable/equatable.dart'; - -import '../types/arweave_address.dart'; +import 'package:pst/pst.dart'; +import 'package:pst/src/utils.dart'; /// Minimum ArDrive community tip from the Community Improvement Proposal Doc: /// https://arweave.net/Yop13NrLwqlm36P_FDCdMaTBwSlj0sdNGAC4FqfRUgo final minArDriveCommunityWinstonTip = Winston(BigInt.from(10000000)); +// TODO: implement unit tests class CommunityOracle { final ContractOracle _contractOracle; + CommunityContractData? _communityContractData; + DateTime? _lastFetchedTime; CommunityOracle(ContractOracle contractOracle) : _contractOracle = contractOracle; Future getCommunityWinstonTip(Winston winstonCost) async { - final tipPercentage = await _contractOracle.getTipPercentageFromContract(); + final contractData = await _getCommunityContractData(); + + final CommunityTipPercentage tipPercentage = + contractData.settings.fee / 100.0; + final value = max( // Workaround [BigInt] percentage division problems // by first multiplying by the percentage * 100 and then dividing by 100. @@ -26,13 +31,15 @@ class CommunityOracle { .toInt(), minArDriveCommunityWinstonTip.value.toInt(), ); + return Winston(BigInt.from(value)); } Future selectTokenHolder({ double? testingRandom, // for testing purposes only }) async { - final contract = await _contractOracle.getCommunityContract(); + final contract = await _getCommunityContractData(); + final Map balances = Map.from(contract.balances); final vault = contract.vault; @@ -73,7 +80,7 @@ class CommunityOracle { final Map weighted = {}; for (final addr in balances.keys) { weighted[addr] = balances[addr]! / total; - } + } // Get a random holder based off of the weighted list of holders final randomHolder = weightedRandom(weighted, testingRandom: testingRandom); @@ -84,6 +91,22 @@ class CommunityOracle { return randomHolder; } + + Future _getCommunityContractData() async { + final currentTime = DateTime.now(); + + if (_communityContractData != null && + _lastFetchedTime != null && + currentTime.difference(_lastFetchedTime!).inMinutes < 30) { + return _communityContractData!; + } + + _communityContractData = await _contractOracle.getCommunityContract(); + + _lastFetchedTime = currentTime; + + return _communityContractData!; + } } class CouldNotDetermineTokenHolder extends Equatable implements Exception { diff --git a/packages/pst/lib/src/constants.dart b/packages/pst/lib/src/constants.dart new file mode 100644 index 0000000000..3484e5612f --- /dev/null +++ b/packages/pst/lib/src/constants.dart @@ -0,0 +1,8 @@ +import 'package:ardrive_utils/ardrive_utils.dart'; + +final pstTransactionId = TransactionID( + '-8A6RexFkpfWwuyVO98wzSFZh0d6VJuI-buTJvlwOJQ', +); +const cacheUrl = 'https://d2440r7x1v6779.cloudfront.net/cache/state'; +const vertoCacheUrl = 'https://v2.cache.verto.exchange'; +const arnsCacheUrl = 'https://dev.arns.app/v1/contract'; diff --git a/lib/pst/contract_oracle.dart b/packages/pst/lib/src/contract_oracle.dart similarity index 73% rename from lib/pst/contract_oracle.dart rename to packages/pst/lib/src/contract_oracle.dart index 2b0c53e42d..baa03a78f3 100644 --- a/lib/pst/contract_oracle.dart +++ b/packages/pst/lib/src/contract_oracle.dart @@ -1,7 +1,7 @@ -import 'package:ardrive/pst/constants.dart'; -import 'package:ardrive/pst/contract_reader.dart'; -import 'package:ardrive/pst/pst_contract_data.dart'; -import 'package:ardrive/pst/pst_contract_data_builder.dart'; +import 'package:pst/src/constants.dart'; +import 'package:pst/src/contract_reader.dart'; +import 'package:pst/src/pst_contract_data.dart'; +import 'package:pst/src/pst_contract_data_builder.dart'; class ContractOracle { final T _contractReader; diff --git a/lib/pst/contract_reader.dart b/packages/pst/lib/src/contract_reader.dart similarity index 62% rename from lib/pst/contract_reader.dart rename to packages/pst/lib/src/contract_reader.dart index e17eacfcca..570394c6a2 100644 --- a/lib/pst/contract_reader.dart +++ b/packages/pst/lib/src/contract_reader.dart @@ -1,4 +1,4 @@ -import 'package:ardrive/types/transaction_id.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; abstract class ContractReader { Future readContract(TransactionID txId); diff --git a/packages/pst/lib/src/contract_readers/arns_contract_reader.dart b/packages/pst/lib/src/contract_readers/arns_contract_reader.dart new file mode 100644 index 0000000000..099bc914b6 --- /dev/null +++ b/packages/pst/lib/src/contract_readers/arns_contract_reader.dart @@ -0,0 +1,14 @@ +import 'package:ardrive_http/ardrive_http.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:pst/pst.dart'; +import 'package:pst/src/constants.dart'; + +class ARNSContractReader implements ContractReader { + @override + Future readContract(TransactionID txId) async { + final apiUrl = '$arnsCacheUrl/$txId'; + final response = await ArDriveHTTP().getJson(apiUrl); + + return response.data['state']; + } +} diff --git a/lib/pst/contract_readers/redstone_contract_reader.dart b/packages/pst/lib/src/contract_readers/redstone_contract_reader.dart similarity index 64% rename from lib/pst/contract_readers/redstone_contract_reader.dart rename to packages/pst/lib/src/contract_readers/redstone_contract_reader.dart index 133cae7cd3..5679843199 100644 --- a/lib/pst/contract_readers/redstone_contract_reader.dart +++ b/packages/pst/lib/src/contract_readers/redstone_contract_reader.dart @@ -1,8 +1,7 @@ -import 'package:ardrive/pst/contract_reader.dart'; -import 'package:ardrive/types/transaction_id.dart'; import 'package:ardrive_http/ardrive_http.dart'; - -const cacheUrl = 'https://d2440r7x1v6779.cloudfront.net/cache/state'; +import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:pst/src/constants.dart'; +import 'package:pst/src/contract_reader.dart'; class RedstoneContractReader implements ContractReader { @override diff --git a/packages/pst/lib/src/contract_readers/smartweave_contract_reader.dart b/packages/pst/lib/src/contract_readers/smartweave_contract_reader.dart new file mode 100644 index 0000000000..5e8aa2e5d2 --- /dev/null +++ b/packages/pst/lib/src/contract_readers/smartweave_contract_reader.dart @@ -0,0 +1,12 @@ +import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:pst/src/contract_reader.dart'; +import 'package:pst/src/implementations/pst_web.dart' + if (dart.library.io) 'package:pst/src/implementations/pst_stub.dart' + as implementation; + +class SmartweaveContractReader implements ContractReader { + @override + Future readContract(TransactionID txId) { + return implementation.readContract(txId); + } +} diff --git a/lib/pst/contract_readers/verto_contract_reader.dart b/packages/pst/lib/src/contract_readers/verto_contract_reader.dart similarity index 58% rename from lib/pst/contract_readers/verto_contract_reader.dart rename to packages/pst/lib/src/contract_readers/verto_contract_reader.dart index 95198a9b28..a9700ad7a0 100644 --- a/lib/pst/contract_readers/verto_contract_reader.dart +++ b/packages/pst/lib/src/contract_readers/verto_contract_reader.dart @@ -1,13 +1,12 @@ -import 'package:ardrive/pst/contract_reader.dart'; -import 'package:ardrive/types/transaction_id.dart'; import 'package:ardrive_http/ardrive_http.dart'; - -const cacheUrl = 'https://v2.cache.verto.exchange'; +import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:pst/src/constants.dart'; +import 'package:pst/src/contract_reader.dart'; class VertoContractReader implements ContractReader { @override Future readContract(TransactionID txId) async { - final apiUrl = '$cacheUrl/$txId'; + final apiUrl = '$vertoCacheUrl/$txId'; final response = await ArDriveHTTP().getJson(apiUrl); return response.data['state']; diff --git a/lib/services/pst/enums.dart b/packages/pst/lib/src/enums.dart similarity index 100% rename from lib/services/pst/enums.dart rename to packages/pst/lib/src/enums.dart diff --git a/lib/services/pst/implementations/pst_stub.dart b/packages/pst/lib/src/implementations/pst_stub.dart similarity index 60% rename from lib/services/pst/implementations/pst_stub.dart rename to packages/pst/lib/src/implementations/pst_stub.dart index 6d439baf1b..40a1b3f679 100644 --- a/lib/services/pst/implementations/pst_stub.dart +++ b/packages/pst/lib/src/implementations/pst_stub.dart @@ -1,3 +1,3 @@ -import 'package:ardrive/types/transaction_id.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; Future readContract(TransactionID txId) => throw UnimplementedError(); diff --git a/lib/services/pst/implementations/pst_web.dart b/packages/pst/lib/src/implementations/pst_web.dart similarity index 83% rename from lib/services/pst/implementations/pst_web.dart rename to packages/pst/lib/src/implementations/pst_web.dart index ae94709e95..1748233ffb 100644 --- a/lib/services/pst/implementations/pst_web.dart +++ b/packages/pst/lib/src/implementations/pst_web.dart @@ -3,7 +3,8 @@ library pst; import 'dart:convert'; -import 'package:ardrive/types/transaction_id.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; +// ignore: depend_on_referenced_packages import 'package:js/js.dart'; import 'package:universal_html/js_util.dart'; diff --git a/lib/services/pst/pst.dart b/packages/pst/lib/src/pst.dart similarity index 81% rename from lib/services/pst/pst.dart rename to packages/pst/lib/src/pst.dart index 885321dbb0..f95ab12084 100644 --- a/lib/services/pst/pst.dart +++ b/packages/pst/lib/src/pst.dart @@ -1,9 +1,7 @@ -import 'package:ardrive/pst/community_oracle.dart'; -import 'package:ardrive/types/arweave_address.dart'; -import 'package:ardrive/types/winston.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; - -import '../services.dart'; +import 'package:pst/src/community_oracle.dart'; +import 'package:pst/src/pst.dart'; export 'enums.dart'; @@ -18,7 +16,7 @@ class PstService { _communityOracle.selectTokenHolder(); Future getPSTFee(BigInt uploadCost) async { - return await _getPSTFee(uploadCost); + return _getPSTFee(uploadCost); } Future _getPSTFee(BigInt uploadCost) async { diff --git a/lib/pst/pst_contract_data.dart b/packages/pst/lib/src/pst_contract_data.dart similarity index 97% rename from lib/pst/pst_contract_data.dart rename to packages/pst/lib/src/pst_contract_data.dart index 1c1933002b..4981549142 100644 --- a/lib/pst/pst_contract_data.dart +++ b/packages/pst/lib/src/pst_contract_data.dart @@ -1,4 +1,4 @@ -import 'package:ardrive/types/arweave_address.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; typedef CommunityTipPercentage = double; diff --git a/lib/pst/pst_contract_data_builder.dart b/packages/pst/lib/src/pst_contract_data_builder.dart similarity index 96% rename from lib/pst/pst_contract_data_builder.dart rename to packages/pst/lib/src/pst_contract_data_builder.dart index 0bb2f61824..b7f688403d 100644 --- a/lib/pst/pst_contract_data_builder.dart +++ b/packages/pst/lib/src/pst_contract_data_builder.dart @@ -1,7 +1,7 @@ -import 'package:ardrive/pst/pst_contract_data.dart'; -import 'package:ardrive/types/arweave_address.dart'; -import 'package:ardrive/utils/logger/logger.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:pst/src/pst_contract_data.dart'; class CommunityContractDataBuilder { final Map _rawData; @@ -208,7 +208,8 @@ class CommunityContractDataBuilder { break; default: - logger.i('Ignoring unknown field: .votes[number].$key = $value'); + // TODO: add the logger package + debugPrint('Ignoring unknown field: .votes[number].$key = $value'); break; } }); @@ -275,7 +276,8 @@ class CommunityContractDataBuilder { break; default: - logger.i( + // TODO: add the logger package + debugPrint( 'Ignoring unknown field: .settings[number][1] ($key: $value)', ); break; @@ -359,7 +361,8 @@ class CommunityContractDataBuilder { } break; default: - logger.i('Ignoring unknown field .vault[address][number].$key'); + // TODO: add the logger package + debugPrint('Ignoring unknown field .vault[address][number].$key'); } }); } diff --git a/lib/pst/utils.dart b/packages/pst/lib/src/utils.dart similarity index 87% rename from lib/pst/utils.dart rename to packages/pst/lib/src/utils.dart index 134e209ace..17334e7a42 100644 --- a/lib/pst/utils.dart +++ b/packages/pst/lib/src/utils.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import '../types/arweave_address.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; ArweaveAddress? weightedRandom( Map dict, { diff --git a/packages/pst/pubspec.yaml b/packages/pst/pubspec.yaml new file mode 100644 index 0000000000..dd75016a9f --- /dev/null +++ b/packages/pst/pubspec.yaml @@ -0,0 +1,34 @@ +name: pst +description: A new Flutter package project. +version: 0.0.1 +homepage: +publish_to: 'none' + +environment: + sdk: '>=3.1.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + arweave: + git: + url: https://github.com/ardriveapp/arweave-dart.git + ref: PE-3697 + ardrive_http: + git: + url: https://github.com/ar-io/ardrive_http.git + ref: v1.3.1 + universal_html: ^2.2.4 + ardrive_utils: + path: ../ardrive_utils + equatable: ^2.0.5 + retry: ^3.1.2 + mocktail: ^0.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: diff --git a/test/pst/community_oracle_test.dart b/packages/pst/test/src/community_oracle_test.dart similarity index 88% rename from test/pst/community_oracle_test.dart rename to packages/pst/test/src/community_oracle_test.dart index 1f9514523d..d4030a4c8a 100644 --- a/test/pst/community_oracle_test.dart +++ b/packages/pst/test/src/community_oracle_test.dart @@ -1,11 +1,9 @@ -import 'package:ardrive/pst/community_oracle.dart'; -import 'package:ardrive/pst/constants.dart'; -import 'package:ardrive/pst/contract_oracle.dart'; -import 'package:ardrive/types/arweave_address.dart'; -import 'package:ardrive/types/transaction_id.dart'; -import 'package:ardrive/types/winston.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:test/test.dart'; +import 'package:pst/src/community_oracle.dart'; +import 'package:pst/src/constants.dart'; +import 'package:pst/src/contract_oracle.dart'; import 'constants.dart'; import 'stubs.dart'; diff --git a/test/pst/constants.dart b/packages/pst/test/src/constants.dart similarity index 99% rename from test/pst/constants.dart rename to packages/pst/test/src/constants.dart index 6ce5d15713..441992b9a7 100644 --- a/test/pst/constants.dart +++ b/packages/pst/test/src/constants.dart @@ -1,4 +1,4 @@ -import 'package:ardrive/pst/pst_contract_data_builder.dart'; +import 'package:pst/src/pst_contract_data_builder.dart'; const Map rawHealthyContractData = { 'balances': { diff --git a/test/pst/contract_oracle_test.dart b/packages/pst/test/src/contract_oracle_test.dart similarity index 93% rename from test/pst/contract_oracle_test.dart rename to packages/pst/test/src/contract_oracle_test.dart index b7534b84d5..d8a6232fd6 100644 --- a/test/pst/contract_oracle_test.dart +++ b/packages/pst/test/src/contract_oracle_test.dart @@ -1,10 +1,10 @@ -import 'package:ardrive/pst/ardrive_contract_oracle.dart'; -import 'package:ardrive/pst/constants.dart'; -import 'package:ardrive/pst/contract_oracle.dart'; -import 'package:ardrive/pst/pst_contract_data.dart'; -import 'package:ardrive/types/transaction_id.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:test/test.dart'; +import 'package:pst/src/ardrive_contract_oracle.dart'; +import 'package:pst/src/constants.dart'; +import 'package:pst/src/contract_oracle.dart'; +import 'package:pst/src/pst_contract_data.dart'; import 'constants.dart'; import 'stubs.dart'; diff --git a/test/pst/pst_contract_data_builder_test.dart b/packages/pst/test/src/pst_contract_data_builder_test.dart similarity index 97% rename from test/pst/pst_contract_data_builder_test.dart rename to packages/pst/test/src/pst_contract_data_builder_test.dart index d7a8d132eb..e87c41a2d7 100644 --- a/test/pst/pst_contract_data_builder_test.dart +++ b/packages/pst/test/src/pst_contract_data_builder_test.dart @@ -1,7 +1,7 @@ -import 'package:ardrive/pst/pst_contract_data.dart'; -import 'package:ardrive/pst/pst_contract_data_builder.dart'; -import 'package:ardrive/types/arweave_address.dart'; -import 'package:test/test.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pst/src/pst_contract_data.dart'; +import 'package:pst/src/pst_contract_data_builder.dart'; import 'constants.dart'; diff --git a/test/pst/stubs.dart b/packages/pst/test/src/stubs.dart similarity index 61% rename from test/pst/stubs.dart rename to packages/pst/test/src/stubs.dart index 39d3026571..2649e8bc68 100644 --- a/test/pst/stubs.dart +++ b/packages/pst/test/src/stubs.dart @@ -1,6 +1,6 @@ -import 'package:ardrive/pst/contract_reader.dart'; -import 'package:ardrive/types/transaction_id.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:pst/src/contract_reader.dart'; class ContractReaderStub extends Mock implements ContractReader { @override diff --git a/test/pst/utils_test.dart b/packages/pst/test/src/utils_test.dart similarity index 86% rename from test/pst/utils_test.dart rename to packages/pst/test/src/utils_test.dart index 157f90e090..ae484c3ca4 100644 --- a/test/pst/utils_test.dart +++ b/packages/pst/test/src/utils_test.dart @@ -1,6 +1,6 @@ -import 'package:ardrive/pst/utils.dart'; -import 'package:ardrive/types/arweave_address.dart'; -import 'package:test/test.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pst/src/utils.dart'; void main() { group('weightedRandom', () { diff --git a/pubspec.lock b/pubspec.lock index cd0f3b4349..19705ed33c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -85,7 +85,7 @@ packages: description: path: "." ref: PE-3699-uploads-large-files-for-public-drives - resolved-ref: "5998dfddf3cf48aa8419b821e373727cb19ae0fa" + resolved-ref: "8c6ac43b14874b7078548b830ad1c8394109491c" url: "https://github.com/ar-io/ardrive_io.git" source: git version: "1.4.0" @@ -139,8 +139,8 @@ packages: dependency: "direct main" description: path: "." - ref: PE-3697 - resolved-ref: "30517c20669de213b8105273d3ec97005867bc90" + ref: PE-4812-cancel-button-fails-to-stop-file-upload-when-using-a-rs-only + resolved-ref: "0fd825555310a1a5bf5d256328c3f207ce148a5e" url: "https://github.com/ardriveapp/arweave-dart.git" source: git version: "3.7.0" @@ -572,9 +572,9 @@ packages: dependency: "direct overridden" description: path: "." - ref: ignore-headers - resolved-ref: ba37ef6eaa291cdb36b4616c6fbec3c690bca728 - url: "https://github.com/karlprieb/fetch_client.git" + ref: PE-4754-address-code-review-comments-on-uploader-downloader-implementations + resolved-ref: "95241dd94f0f64bae7553e7a3bbaa5d0558f63ee" + url: "https://github.com/thiagocarvalhodev/fetch_client.git" source: git version: "1.0.2" ffi: @@ -1283,6 +1283,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + loading_animation_widget: + dependency: "direct main" + description: + name: loading_animation_widget + sha256: "1901682600273a966c34cf44a85fc5355da92a8d08a8a43c11adc4e471993e3a" + url: "https://pub.dev" + source: hosted + version: "1.2.0+4" local_auth: dependency: "direct main" description: @@ -1611,6 +1619,13 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.5" + pst: + dependency: "direct main" + description: + path: "packages/pst" + relative: true + source: path + version: "0.0.1" pub_semver: dependency: transitive description: @@ -2291,5 +2306,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0 <4.0.0" - flutter: ">=3.13.0" + dart: ">=3.1.3 <4.0.0" + flutter: ">=3.13.2" diff --git a/pubspec.yaml b/pubspec.yaml index db5698d107..ce0732c438 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ version: 2.21.0 environment: sdk: '>=3.0.2 <4.0.0' - flutter: 3.10.2 + flutter: 3.13.2 # https://pub.dev/packages/script_runner script_runner: @@ -49,6 +49,8 @@ dependencies: ardrive_crypto: path: ./packages/ardrive_crypto artemis: ^7.0.0-beta.13 + pst: + path: ./packages/pst arweave: git: url: https://github.com/ardriveapp/arweave-dart.git @@ -125,6 +127,7 @@ dependencies: dio: ^5.3.2 provider: ^6.0.5 just_audio: ^0.9.34 + loading_animation_widget: ^1.2.0+4 synchronized: ^3.1.0 dependency_overrides: @@ -135,7 +138,7 @@ dependency_overrides: arweave: git: url: https://github.com/ardriveapp/arweave-dart.git - ref: PE-3697 + ref: PE-4812-cancel-button-fails-to-stop-file-upload-when-using-a-rs-only stripe_js: git: url: https://github.com/ardriveapp/flutter_stripe/ @@ -148,8 +151,8 @@ dependency_overrides: ref: main fetch_client: git: - url: https://github.com/karlprieb/fetch_client.git - ref: ignore-headers + url: https://github.com/thiagocarvalhodev/fetch_client.git + ref: PE-4754-address-code-review-comments-on-uploader-downloader-implementations http: ^1.1.0 dev_dependencies: diff --git a/test/blocs/create_snapshot_cubit_test.dart b/test/blocs/create_snapshot_cubit_test.dart index 332c8f04a8..5860648004 100644 --- a/test/blocs/create_snapshot_cubit_test.dart +++ b/test/blocs/create_snapshot_cubit_test.dart @@ -5,9 +5,9 @@ import 'package:ardrive/entities/snapshot_entity.dart'; import 'package:ardrive/services/config/app_config.dart'; import 'package:ardrive/turbo/services/payment_service.dart'; import 'package:ardrive/turbo/services/upload_service.dart'; -import 'package:ardrive/types/winston.dart'; import 'package:ardrive/user/user.dart'; import 'package:ardrive/utils/snapshots/range.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; import 'package:arweave/utils.dart'; import 'package:bloc_test/bloc_test.dart'; diff --git a/test/blocs/upload_cubit_test.dart b/test/blocs/upload_cubit_test.dart index ba49351e13..298ebd8d8b 100644 --- a/test/blocs/upload_cubit_test.dart +++ b/test/blocs/upload_cubit_test.dart @@ -13,19 +13,19 @@ import 'package:ardrive/entities/profile_types.dart'; import 'package:ardrive/models/daos/drive_dao/drive_dao.dart'; import 'package:ardrive/models/database/database.dart'; import 'package:ardrive/services/arweave/arweave.dart'; -import 'package:ardrive/services/pst/pst.dart'; import 'package:ardrive/turbo/services/upload_service.dart'; import 'package:ardrive/turbo/turbo.dart'; -import 'package:ardrive/types/winston.dart'; import 'package:ardrive/user/user.dart'; import 'package:ardrive/utils/upload_plan_utils.dart'; import 'package:ardrive_io/ardrive_io.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:cryptography/cryptography.dart'; import 'package:cryptography/helpers.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:pst/pst.dart'; import '../core/upload/uploader_test.dart'; import '../test_utils/utils.dart'; @@ -265,6 +265,7 @@ void main() { targetFolder: any(named: 'targetFolder'))) .thenAnswer((invocation) => Future.value( UploadPlan.create( + useTurbo: false, maxDataItemCount: 10, fileV2UploadHandles: {}, fileDataItemUploadHandles: {}, @@ -398,6 +399,7 @@ void main() { targetFolder: any(named: 'targetFolder'))).thenAnswer( (invocation) => Future.value( UploadPlan.create( + useTurbo: false, maxDataItemCount: 10, turboUploadService: DontUseUploadService(), fileV2UploadHandles: {}, @@ -501,6 +503,7 @@ void main() { targetFolder: any(named: 'targetFolder'))).thenAnswer( (invocation) => Future.value( UploadPlan.create( + useTurbo: false, maxDataItemCount: 10, fileV2UploadHandles: {}, fileDataItemUploadHandles: {}, diff --git a/test/core/upload/cost_calculator_test.dart b/test/core/upload/cost_calculator_test.dart index 4b6e6de4e6..9974d7d002 100644 --- a/test/core/upload/cost_calculator_test.dart +++ b/test/core/upload/cost_calculator_test.dart @@ -1,8 +1,9 @@ import 'package:ardrive/core/upload/cost_calculator.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/turbo/turbo.dart'; -import 'package:ardrive/types/winston.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:pst/pst.dart'; import 'package:test/test.dart'; // We start by creating mocks for the services that will be used diff --git a/test/download/download_utils_test.dart b/test/download/download_utils_test.dart index 0c3b020b48..fb0a8c422b 100644 --- a/test/download/download_utils_test.dart +++ b/test/download/download_utils_test.dart @@ -1,7 +1,7 @@ import 'package:ardrive/core/arfs/repository/arfs_repository.dart'; import 'package:ardrive/download/download_utils.dart'; import 'package:ardrive/models/daos/drive_dao/drive_dao.dart'; -import 'package:ardrive/utils/data_size.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; diff --git a/test/download/multiple_download_bloc_test.dart b/test/download/multiple_download_bloc_test.dart index ced5b1eddc..8b2574113d 100644 --- a/test/download/multiple_download_bloc_test.dart +++ b/test/download/multiple_download_bloc_test.dart @@ -7,7 +7,6 @@ import 'package:ardrive/download/download_utils.dart'; import 'package:ardrive/download/multiple_download_bloc.dart'; import 'package:ardrive/models/daos/daos.dart'; import 'package:ardrive/services/arweave/arweave.dart'; -import 'package:ardrive/utils/data_size.dart'; import 'package:ardrive_http/ardrive_http.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:bloc_test/bloc_test.dart'; diff --git a/test/test_utils/mocks.dart b/test/test_utils/mocks.dart index 59289af150..dc8b1469da 100644 --- a/test/test_utils/mocks.dart +++ b/test/test_utils/mocks.dart @@ -23,6 +23,7 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/widgets.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:pst/pst.dart'; class MockArweave extends Mock implements Arweave {} diff --git a/test/turbo/turbo_test.dart b/test/turbo/turbo_test.dart index 4fa519c8ec..14a2f0d745 100644 --- a/test/turbo/turbo_test.dart +++ b/test/turbo/turbo_test.dart @@ -4,9 +4,9 @@ import 'package:ardrive/turbo/services/payment_service.dart'; import 'package:ardrive/turbo/topup/models/payment_model.dart'; import 'package:ardrive/turbo/topup/models/price_estimate.dart'; import 'package:ardrive/turbo/turbo.dart'; -import 'package:ardrive/utils/data_size.dart'; import 'package:ardrive/utils/file_size_units.dart'; import 'package:ardrive/utils/logger/logger.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; // ignore: depend_on_referenced_packages import 'package:fake_async/fake_async.dart'; diff --git a/test/turbo/utils/utils_test.dart b/test/turbo/utils/utils_test.dart index 999b350cd1..90a8f4b1dd 100644 --- a/test/turbo/utils/utils_test.dart +++ b/test/turbo/utils/utils_test.dart @@ -2,13 +2,13 @@ import 'package:ardrive/turbo/utils/utils.dart'; import 'package:test/test.dart'; void main() { - group('convertCreditsToLiteralString', () { + group('convertWinstonToLiteralString', () { test('returns correct literal credits string', () { expect( - convertCreditsToLiteralString(BigInt.from(5000000000000)), '5.0000'); + convertWinstonToLiteralString(BigInt.from(5000000000000)), '5.0000'); expect( - convertCreditsToLiteralString(BigInt.from(1234567890000)), '1.2346'); - expect(convertCreditsToLiteralString(BigInt.from(0)), '0.0000'); + convertWinstonToLiteralString(BigInt.from(1234567890000)), '1.2346'); + expect(convertWinstonToLiteralString(BigInt.from(0)), '0.0000'); }); }); diff --git a/test/utils/utils_test.dart b/test/utils/utils_test.dart index 926c42c12a..80cfe5416e 100644 --- a/test/utils/utils_test.dart +++ b/test/utils/utils_test.dart @@ -11,7 +11,7 @@ void main() { group('convertCreditsToLiteralString method', () { test('should return 0.0001 when credits is 100000000', () { - final result = convertCreditsToLiteralString(BigInt.from(100000000)); + final result = convertWinstonToLiteralString(BigInt.from(100000000)); expect(result, '0.0001'); }); }); diff --git a/web/index.html b/web/index.html index 423ec985a1..394afa588b 100644 --- a/web/index.html +++ b/web/index.html @@ -18,6 +18,7 @@ + diff --git a/web/js/StreamSaver.min.js b/web/js/StreamSaver.min.js new file mode 100644 index 0000000000..3f664a77e9 --- /dev/null +++ b/web/js/StreamSaver.min.js @@ -0,0 +1,9 @@ +/** + * Minified by jsDelivr using Terser v5.10.0. + * Original file: https://cdn.jsdelivr.net//npm/streamsaver@2.0.6/StreamSaver.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +/*! streamsaver. MIT License. Jimmy Wärting */ +((e,t)=>{"undefined"!=typeof module?module.exports=t():"function"==typeof define&&"object"==typeof define.amd?define(t):this.streamSaver=t()})(0,(()=>{"use strict";const e="object"==typeof window?window:this;e.HTMLElement||console.warn("streamsaver is meant to run on browsers main thread");let t=null,a=!1;const r=e.WebStreamsPolyfill||{},n=e.isSecureContext;let o=/constructor/i.test(e.HTMLElement)||!!e.safari||!!e.WebKitPoint;const s=n||"MozAppearance"in document.documentElement.style?"iframe":"navigate",i={createWriteStream:function(r,m,c){let d={size:null,pathname:null,writableStrategy:void 0,readableStrategy:void 0},p=0,u=null,f=null,g=null;Number.isFinite(m)?([c,m]=[m,c],console.warn("[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream"),d.size=c,d.writableStrategy=m):m&&m.highWaterMark?(console.warn("[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream"),d.size=c,d.writableStrategy=m):d=m||{};if(!o){t||(t=n?l(i.mitm):function(t){const a="width=200,height=100",r=document.createDocumentFragment(),n={frame:e.open(t,"popup",a),loaded:!1,isIframe:!1,isPopup:!0,remove(){n.frame.close()},addEventListener(...e){r.addEventListener(...e)},dispatchEvent(...e){r.dispatchEvent(...e)},removeEventListener(...e){r.removeEventListener(...e)},postMessage(...e){n.frame.postMessage(...e)}},o=t=>{t.source===n.frame&&(n.loaded=!0,e.removeEventListener("message",o),n.dispatchEvent(new Event("load")))};return e.addEventListener("message",o),n}(i.mitm)),f=new MessageChannel,r=encodeURIComponent(r.replace(/\//g,":")).replace(/['()]/g,escape).replace(/\*/g,"%2A");const o={transferringReadable:a,pathname:d.pathname||Math.random().toString().slice(-6)+"/"+r,headers:{"Content-Type":"application/octet-stream; charset=utf-8","Content-Disposition":"attachment; filename*=UTF-8''"+r}};d.size&&(o.headers["Content-Length"]=d.size);const m=[o,"*",[f.port2]];if(a){const e="iframe"===s?void 0:{transform(e,t){if(!(e instanceof Uint8Array))throw new TypeError("Can only write Uint8Arrays");p+=e.length,t.enqueue(e),u&&(location.href=u,u=null)},flush(){u&&(location.href=u)}};g=new i.TransformStream(e,d.writableStrategy,d.readableStrategy);const t=g.readable;f.port1.postMessage({readableStream:t},[t])}f.port1.onmessage=e=>{e.data.download?"navigate"===s?(t.remove(),t=null,p?location.href=e.data.download:u=e.data.download):(t.isPopup&&(t.remove(),t=null,"iframe"===s&&l(i.mitm)),l(e.data.download)):e.data.abort&&(h=[],f.port1.postMessage("abort"),f.port1.onmessage=null,f.port1.close(),f.port2.close(),f=null)},t.loaded?t.postMessage(...m):t.addEventListener("load",(()=>{t.postMessage(...m)}),{once:!0})}let h=[];return!o&&g&&g.writable||new i.WritableStream({write(e){if(!(e instanceof Uint8Array))throw new TypeError("Can only write Uint8Arrays");o?h.push(e):(f.port1.postMessage(e),p+=e.length,u&&(location.href=u,u=null))},close(){if(o){const e=new Blob(h,{type:"application/octet-stream; charset=utf-8"}),t=document.createElement("a");t.href=URL.createObjectURL(e),t.download=r,t.click()}else f.port1.postMessage("end")},abort(){h=[],f.port1.postMessage("abort"),f.port1.onmessage=null,f.port1.close(),f.port2.close(),f=null}},d.writableStrategy)},WritableStream:e.WritableStream||r.WritableStream,supported:!0,version:{full:"2.0.5",major:2,minor:0,dot:5},mitm:"https://fyoh6lxmopwn2zmdkobqfqmmwpyfq6c2xeyqp5lm7bhlrg7slyja.arweave.net/Lhx_Luxz7N1lg1ODAsGMs_BYeFq5MQf1bPhOuJvyXhI/mitm.html"};function l(e){if(!e)throw new Error("meh");const t=document.createElement("iframe");return t.hidden=!0,t.src=e,t.loaded=!1,t.name="iframe",t.isIframe=!0,t.postMessage=(...e)=>t.contentWindow.postMessage(...e),t.addEventListener("load",(()=>{t.loaded=!0}),{once:!0}),document.body.appendChild(t),t}try{new Response(new ReadableStream),n&&!("serviceWorker"in navigator)&&(o=!0)}catch(e){o=!0}return(e=>{try{e()}catch(e){}})((()=>{const{readable:e}=new TransformStream,t=new MessageChannel;t.port1.postMessage(e,[e]),t.port1.close(),t.port2.close(),a=!0,Object.defineProperty(i,"TransformStream",{configurable:!1,writable:!1,value:TransformStream})})),i})); +//# sourceMappingURL=https://cdn.jsdelivr.net/sm/a47e90c9e77f79e22405531b2deda4d299dc4ffb8262b6b5b3af3580ec770db0.map \ No newline at end of file