diff --git a/lib/blocs/create_manifest/create_manifest_cubit.dart b/lib/blocs/create_manifest/create_manifest_cubit.dart index 9dd5fbda31..fbc01c518e 100644 --- a/lib/blocs/create_manifest/create_manifest_cubit.dart +++ b/lib/blocs/create_manifest/create_manifest_cubit.dart @@ -223,13 +223,7 @@ class CreateManifestCubit extends Cubit { bundle.blob, wallet, ); - - // Add tips to bundle tx - final bundleTip = await _pst.getPSTFee(bundleTx.reward); - bundleTx - ..addTag(TipType.tagName, TipType.dataUpload) - ..setTarget(await _pst.getWeightedPstHolder()) - ..setQuantity(bundleTip); + await _pst.addCommunityTipToTx(bundleTx); final totalCost = bundleTx.reward + bundleTx.quantity; diff --git a/lib/blocs/drives/drives_cubit.dart b/lib/blocs/drives/drives_cubit.dart index e8eb94c1cf..d24a08e8c5 100644 --- a/lib/blocs/drives/drives_cubit.dart +++ b/lib/blocs/drives/drives_cubit.dart @@ -72,6 +72,7 @@ class DrivesCubit extends Cubit { void selectDrive(String driveId) { final canCreateNewDrive = _profileCubit.state is ProfileLoggedIn; + final state = this.state is DrivesLoadSuccess ? (this.state as DrivesLoadSuccess).copyWith(selectedDriveId: driveId) : DrivesLoadSuccess( diff --git a/lib/blocs/upload/cost_estimate.dart b/lib/blocs/upload/cost_estimate.dart index 1ec8f32256..73a7f1715c 100644 --- a/lib/blocs/upload/cost_estimate.dart +++ b/lib/blocs/upload/cost_estimate.dart @@ -1,14 +1,10 @@ import 'package:ardrive/blocs/upload/models/upload_plan.dart'; import 'package:ardrive/blocs/upload/upload_handles/bundle_upload_handle.dart'; import 'package:ardrive/blocs/upload/upload_handles/file_v2_upload_handle.dart'; -import 'package:ardrive/entities/entity.dart'; import 'package:ardrive/services/arweave/arweave.dart'; import 'package:ardrive/services/pst/pst.dart'; import 'package:arweave/arweave.dart'; import 'package:arweave/utils.dart'; -import 'package:package_info_plus/package_info_plus.dart'; - -final minimumPstTip = BigInt.from(10000000); class CostEstimate { /// The cost to upload the data, in AR. @@ -25,15 +21,11 @@ class CostEstimate { /// The sum of the upload cost and fees. final BigInt totalCost; - /// The [Transaction] that pays `pstFee` to a random PST holder. (Only for v2 transaction uploads) - final Transaction? v2FilesFeeTx; - CostEstimate._create({ required this.arUploadCost, required this.pstFee, required this.totalCost, this.usdUploadCost, - this.v2FilesFeeTx, }); static Future create({ @@ -43,6 +35,7 @@ class CostEstimate { required Wallet wallet, }) async { final _v2FileUploadHandles = uploadPlan.fileV2UploadHandles; + final dataItemsCost = await estimateCostOfAllBundles( bundleUploadHandles: uploadPlan.bundleUploadHandles, arweaveService: arweaveService, @@ -52,17 +45,10 @@ class CostEstimate { arweaveService: arweaveService); final bundlePstFee = await pstService.getPSTFee(dataItemsCost); + final v2FilesPstFee = v2FilesUploadCost <= BigInt.zero + ? BigInt.zero + : await pstService.getPSTFee(v2FilesUploadCost); - Transaction? v2FilesFeeTx; - if (_v2FileUploadHandles.isNotEmpty) { - v2FilesFeeTx = await prepareAndSignV2FilesTipTx( - arweaveService: arweaveService, - pstService: pstService, - wallet: wallet, - v2FilesUploadCost: v2FilesUploadCost, - ); - } - final v2FilesPstFee = (v2FilesFeeTx?.quantity ?? BigInt.zero); final totalCost = v2FilesUploadCost + dataItemsCost + bundlePstFee + v2FilesPstFee; @@ -70,42 +56,15 @@ class CostEstimate { final usdUploadCost = await arweaveService .getArUsdConversionRate() .then((conversionRate) => double.parse(arUploadCost) * conversionRate); + return CostEstimate._create( totalCost: totalCost, arUploadCost: arUploadCost, pstFee: v2FilesPstFee + bundlePstFee, usdUploadCost: usdUploadCost, - v2FilesFeeTx: v2FilesFeeTx, ); } - static Future prepareAndSignV2FilesTipTx({ - required ArweaveService arweaveService, - required PstService pstService, - required Wallet wallet, - required v2FilesUploadCost, - }) async { - if (v2FilesUploadCost <= BigInt.zero) { - return null; - } - final packageInfo = await PackageInfo.fromPlatform(); - final pstFee = await pstService.getPSTFee(v2FilesUploadCost); - final quantity = pstFee > minimumPstTip ? pstFee : minimumPstTip; - - final feeTx = await arweaveService.client.transactions.prepare( - Transaction( - target: await pstService.getWeightedPstHolder(), - quantity: quantity, - ), - wallet, - ) - ..addApplicationTags(version: packageInfo.version) - ..addTag('Type', 'fee') - ..addTag(TipType.tagName, TipType.dataUpload); - await feeTx.sign(wallet); - return feeTx; - } - static Future estimateCostOfAllBundles({ required List bundleUploadHandles, required ArweaveService arweaveService, diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index 90805834c3..2741f249ad 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -19,7 +19,6 @@ part 'upload_state.dart'; final privateFileSizeLimit = 104857600; final publicFileSizeLimit = 1.25 * math.pow(10, 9); -final minimumPstTip = BigInt.from(10000000); final filesNamesToExclude = ['.DS_Store']; class UploadCubit extends Cubit { @@ -294,10 +293,6 @@ class UploadCubit extends Cubit { ), ); - if (costEstimate.v2FilesFeeTx != null) { - await _arweave.postTx(costEstimate.v2FilesFeeTx!); - } - // Upload Bundles for (var bundleHandle in uploadPlan.bundleUploadHandles) { await bundleHandle.prepareAndSignBundleTransaction( @@ -318,9 +313,7 @@ class UploadCubit extends Cubit { // Upload V2 Files for (final uploadHandle in uploadPlan.fileV2UploadHandles.values) { await uploadHandle.prepareAndSignTransactions( - arweaveService: _arweave, - wallet: profile.wallet, - ); + arweaveService: _arweave, wallet: profile.wallet, pstService: _pst); await uploadHandle.writeFileEntityToDatabase( driveDao: _driveDao, ); diff --git a/lib/blocs/upload/upload_handles/bundle_upload_handle.dart b/lib/blocs/upload/upload_handles/bundle_upload_handle.dart index 5b23ebf56b..57cc40354c 100644 --- a/lib/blocs/upload/upload_handles/bundle_upload_handle.dart +++ b/lib/blocs/upload/upload_handles/bundle_upload_handle.dart @@ -61,13 +61,11 @@ class BundleUploadHandle implements UploadHandle { wallet, ); - // Add tips to bundle tx - final bundleTip = await pstService.getPSTFee(bundleTx.reward); - bundleTx - ..addTag(TipType.tagName, TipType.dataUpload) - ..setTarget(await pstService.getWeightedPstHolder()) - ..setQuantity(bundleTip); + await pstService.addCommunityTipToTx(bundleTx); + await bundleTx.sign(wallet); + + // Write entities to database folderDataItemUploadHandles.forEach((folder) async { await folder.writeFolderToDatabase(driveDao: driveDao); }); 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 93c624f86c..0fbaf6a5b5 100644 --- a/lib/blocs/upload/upload_handles/file_v2_upload_handle.dart +++ b/lib/blocs/upload/upload_handles/file_v2_upload_handle.dart @@ -52,8 +52,11 @@ class FileV2UploadHandle implements UploadHandle { }); } - Future prepareAndSignTransactions( - {required ArweaveService arweaveService, required Wallet wallet}) async { + Future prepareAndSignTransactions({ + required ArweaveService arweaveService, + required Wallet wallet, + required PstService pstService, + }) async { final packageInfo = await PackageInfo.fromPlatform(); final fileData = await file.readAsBytes(); @@ -62,9 +65,10 @@ class FileV2UploadHandle implements UploadHandle { ? await createEncryptedTransaction(fileData, fileKey!) : Transaction.withBlobData(data: fileData), wallet, - ); + ) + ..addApplicationTags(version: packageInfo.version); - dataTx.addApplicationTags(version: packageInfo.version); + await pstService.addCommunityTipToTx(dataTx); // Don't include the file's Content-Type tag if it is meant to be private. if (!isPrivate) { diff --git a/lib/components/app_drawer/app_drawer.dart b/lib/components/app_drawer/app_drawer.dart index 6cfcb96140..387cff300c 100644 --- a/lib/components/app_drawer/app_drawer.dart +++ b/lib/components/app_drawer/app_drawer.dart @@ -179,75 +179,79 @@ class AppDrawer extends StatelessWidget { if (profileState.runtimeType == ProfileLoggedIn) { final profile = profileState as ProfileLoggedIn; + final hasMinBalance = profile.walletBalance >= minimumWalletBalance; + return Column( + children: [ + ListTileTheme( + textColor: theme.textTheme.bodyText1!.color, + iconColor: theme.iconTheme.color, + child: Align( + alignment: Alignment.center, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + child: BlocBuilder( + builder: (context, state) => PopupMenuButton( + onSelected: (callback) => callback(context), + itemBuilder: (context) => [ + if (state is DriveDetailLoadSuccess) ...{ + _buildNewFolderItem(context, state, hasMinBalance), + PopupMenuDivider(), + _buildUploadFileItem(context, state, hasMinBalance), + _buildUploadFolderItem(context, state, hasMinBalance), + PopupMenuDivider(), + }, + if (drivesState is DrivesLoadSuccess) ...{ + _buildCreateDrive(context, drivesState, hasMinBalance), + _buildAttachDrive(context) + }, + if (state is DriveDetailLoadSuccess && + state.currentDrive.privacy == 'public') ...{ + _buildCreateManifestItem(context, state, hasMinBalance) + }, + ], + child: _buildNewButton(context), + ), + ), + ), + ), + ), + if (!hasMinBalance) ...{ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + appLocalizationsOf(context).insufficientARWarning, + style: Theme.of(context) + .textTheme + .caption! + .copyWith(color: Colors.grey), + ), + ), + TextButton( + onPressed: () => launch(R.arHelpLink), + child: Text( + appLocalizationsOf(context).howDoIGetAR, + style: TextStyle( + color: Colors.grey, + decoration: TextDecoration.underline, + ), + ), + ), + } + ], + ); + } else { return ListTileTheme( textColor: theme.textTheme.bodyText1!.color, iconColor: theme.iconTheme.color, child: Align( alignment: Alignment.center, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), - child: BlocBuilder( - builder: (context, state) => profile.walletBalance >= - minimumWalletBalance - ? PopupMenuButton( - onSelected: (callback) => callback(context), - itemBuilder: (context) => [ - if (state is DriveDetailLoadSuccess) ...{ - PopupMenuItem( - enabled: state.hasWritePermissions, - value: (context) => promptToCreateFolder( - context, - driveId: state.currentDrive.id, - parentFolderId: state.folderInView.folder.id, - ), - child: ListTile( - enabled: state.hasWritePermissions, - title: - Text(appLocalizationsOf(context).newFolder), - ), - ), - PopupMenuDivider(), - PopupMenuItem( - enabled: state.hasWritePermissions, - value: (context) => promptToUpload( - context, - driveId: state.currentDrive.id, - folderId: state.folderInView.folder.id, - isFolderUpload: false, - ), - child: ListTile( - enabled: state.hasWritePermissions, - title: Text( - appLocalizationsOf(context).uploadFiles, - ), - ), - ), - PopupMenuItem( - enabled: state.hasWritePermissions, - value: (context) => promptToUpload( - context, - driveId: state.currentDrive.id, - folderId: state.folderInView.folder.id, - isFolderUpload: true, - ), - child: ListTile( - enabled: state.hasWritePermissions, - title: Text( - appLocalizationsOf(context).uploadFolder, - ), - ), - ), - PopupMenuDivider(), - }, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + child: PopupMenuButton( + onSelected: (callback) => callback(context), + itemBuilder: (context) => [ if (drivesState is DrivesLoadSuccess) ...{ - PopupMenuItem( - enabled: drivesState.canCreateNewDrive, - value: (context) => promptToCreateDrive(context), - child: ListTile( - enabled: drivesState.canCreateNewDrive, - title: Text(appLocalizationsOf(context).newDrive), - ), - ), PopupMenuItem( value: (context) => attachDrive(context: context), child: ListTile( @@ -255,125 +259,143 @@ class AppDrawer extends StatelessWidget { Text(appLocalizationsOf(context).attachDrive), ), ), - }, - if (state is DriveDetailLoadSuccess && - state.currentDrive.privacy == 'public') ...{ - PopupMenuItem( - value: (context) => promptToCreateManifest(context, - drive: state.currentDrive), - enabled: !state.driveIsEmpty, - child: ListTile( - title: Text( - appLocalizationsOf(context).createManifest, - ), - enabled: !state.driveIsEmpty, - ), - ), - }, + } ], - child: SizedBox( - width: 164, - height: 36, - child: FloatingActionButton.extended( - onPressed: null, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - label: Text( - appLocalizationsOf(context).newStringEmphasized, - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: 164, - height: 36, - child: FloatingActionButton.extended( - onPressed: null, - backgroundColor: Colors.grey, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - label: Text( - appLocalizationsOf(context).newStringEmphasized, - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - appLocalizationsOf(context).insufficientARWarning, - style: Theme.of(context) - .textTheme - .caption! - .copyWith(color: Colors.grey), - ), - ), - TextButton( - onPressed: () => launch(R.arHelpLink), - child: Text( - appLocalizationsOf(context).howDoIGetAR, - style: TextStyle( - color: Colors.grey, - decoration: TextDecoration.underline, - ), - ), - ), - ], - ), - ), - ), - ), - ); - } else { - return ListTileTheme( - textColor: theme.textTheme.bodyText1!.color, - iconColor: theme.iconTheme.color, - child: Align( - alignment: Alignment.center, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), - child: PopupMenuButton( - onSelected: (callback) => callback(context), - itemBuilder: (context) => [ - if (drivesState is DrivesLoadSuccess) ...{ - PopupMenuItem( - value: (context) => attachDrive(context: context), - child: ListTile( - title: Text(appLocalizationsOf(context).attachDrive), - ), - ), - } - ], - child: SizedBox( - width: 164, - height: 36, - child: FloatingActionButton.extended( - onPressed: null, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - label: Text( - appLocalizationsOf(context).newStringEmphasized, - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - ), - )), + child: _buildNewButton(context))), ), ); } } + PopupMenuEntry _buildNewFolderItem( + context, DriveDetailLoadSuccess state, bool hasMinBalance) { + return _buildMenuItemTile( + context: context, + isEnabled: state.hasWritePermissions && hasMinBalance, + itemTitle: appLocalizationsOf(context).newFolder, + message: hasMinBalance + ? null + : appLocalizationsOf(context).insufficientFundsForCreateAFolder, + value: (context) => promptToCreateFolder( + context, + driveId: state.currentDrive.id, + parentFolderId: state.folderInView.folder.id, + ), + ); + } + + PopupMenuEntry _buildUploadFileItem( + context, DriveDetailLoadSuccess state, bool hasMinBalance) { + return _buildMenuItemTile( + context: context, + isEnabled: state.hasWritePermissions && hasMinBalance, + message: hasMinBalance + ? null + : appLocalizationsOf(context).insufficientFundsForUploadFiles, + itemTitle: appLocalizationsOf(context).uploadFiles, + value: (context) => promptToUpload( + context, + driveId: state.currentDrive.id, + folderId: state.folderInView.folder.id, + isFolderUpload: false, + ), + ); + } + + PopupMenuEntry _buildUploadFolderItem( + context, DriveDetailLoadSuccess state, bool hasMinBalance) { + return _buildMenuItemTile( + context: context, + isEnabled: state.hasWritePermissions && hasMinBalance, + itemTitle: appLocalizationsOf(context).uploadFolder, + message: hasMinBalance + ? null + : appLocalizationsOf(context).insufficientFundsForUploadFolders, + value: (context) => promptToUpload( + context, + driveId: state.currentDrive.id, + folderId: state.folderInView.folder.id, + isFolderUpload: true, + ), + ); + } + + PopupMenuEntry _buildAttachDrive(BuildContext context) { + return PopupMenuItem( + value: (context) => attachDrive(context: context), + child: ListTile( + title: Text(appLocalizationsOf(context).attachDrive), + ), + ); + } + + PopupMenuEntry _buildCreateDrive(BuildContext context, + DrivesLoadSuccess drivesState, bool hasMinBalance) { + return _buildMenuItemTile( + context: context, + isEnabled: drivesState.canCreateNewDrive && hasMinBalance, + itemTitle: appLocalizationsOf(context).newDrive, + message: hasMinBalance + ? null + : appLocalizationsOf(context).insufficientFundsForCreateADrive, + value: (context) => promptToCreateDrive(context), + ); + } + + Widget _buildNewButton(BuildContext context) { + return SizedBox( + width: 164, + height: 36, + child: FloatingActionButton.extended( + onPressed: null, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + label: Text( + appLocalizationsOf(context).newStringEmphasized, + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } + + PopupMenuEntry _buildCreateManifestItem(BuildContext context, + DriveDetailLoadSuccess state, bool hasMinBalance) { + return _buildMenuItemTile( + context: context, + isEnabled: !state.driveIsEmpty, + itemTitle: appLocalizationsOf(context).createManifest, + message: hasMinBalance + ? null + : appLocalizationsOf(context).insufficientFundsForCreateAManifest, + value: (context) => + promptToCreateManifest(context, drive: state.currentDrive), + ); + } + + PopupMenuEntry _buildMenuItemTile( + {required bool isEnabled, + Future Function(dynamic)? value, + String? message, + required String itemTitle, + required BuildContext context}) { + return PopupMenuItem( + value: value, + enabled: isEnabled, + child: Tooltip( + message: message ?? '', + child: ListTile( + textColor: + isEnabled ? ListTileTheme.of(context).textColor : Colors.grey, + title: Text( + itemTitle, + ), + enabled: isEnabled, + ), + ), + ); + } + Widget _buildSyncButton() => BlocBuilder( builder: (context, syncState) => IconButton( icon: const Icon(Icons.refresh), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3129603156..99a474b419 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1115,6 +1115,26 @@ } } }, + "insufficientFundsForCreateADrive": "You do not have sufficient funds to create a Drive at this time. Please go to the top up page to add funds to your account.", + "@insufficientFundsForCreateADrive": { + "description": "Show that needs funds to create a Drive" + }, + "insufficientFundsForCreateAFolder": "You do not have sufficient funds to create a Folder at this time. Please go to the top up page to add funds to your account.", + "@insufficientFundsForCreateADrive": { + "description": "Show that needs funds to create a Folder" + }, + "insufficientFundsForCreateAManifest": "You do not have sufficient funds to create a Manifest at this time. Please go to the top up page to add funds to your account.", + "@insufficientFundsForCreateAManifest": { + "description": "Show that needs funds to create a Manifest" + }, + "insufficientFundsForUploadFiles": "You do not have sufficient funds to upload Files at this time. Please go to the top up page to add funds to your account.", + "@insufficientFundsForUploadFiles": { + "description": "Show that needs funds to upload files" + }, + "insufficientFundsForUploadFolders": "You do not have sufficient funds to upload Folders at this time. Please go to the top up page to add funds to your account.", + "@insufficientFundsForUploadFolders": { + "description": "Show that needs funds to upload folders" + }, "conflictingNameFound": "Conflicting name was found", "@conflictingNameFound": { "description": "There is a non-manifest entity with the same name" diff --git a/lib/pages/app_router_delegate.dart b/lib/pages/app_router_delegate.dart index 2f09cc8750..2b80ec3f6b 100644 --- a/lib/pages/app_router_delegate.dart +++ b/lib/pages/app_router_delegate.dart @@ -116,6 +116,7 @@ class AppRouterDelegate extends RouterDelegate if (state is DrivesLoadSuccess) { shellPage = !state.hasNoDrives ? DriveDetailPage() : NoDrivesPage(); + driveId = state.selectedDriveId; } shellPage ??= const SizedBox(); diff --git a/lib/services/pst/pst.dart b/lib/services/pst/pst.dart index a7b6ff85ee..299dca5fb9 100644 --- a/lib/services/pst/pst.dart +++ b/lib/services/pst/pst.dart @@ -1,8 +1,13 @@ +import 'package:arweave/arweave.dart'; + +import '../services.dart'; import 'implementations/pst_web.dart' if (dart.library.io) 'implementations/pst_stub.dart' as implementation; export 'enums.dart'; +final minimumPstTip = BigInt.from(10000000); + class PstService { /// Returns the fee percentage of the app PST as a decimal percentage. Future getPstFeePercentage() => implementation.getPstFeePercentage(); @@ -12,6 +17,14 @@ class PstService { implementation.getWeightedPstHolder(); Future getPSTFee(BigInt uploadCost) async { + final pstFee = await _getPSTFee(uploadCost); + if (pstFee > minimumPstTip) { + return pstFee; + } + return minimumPstTip; + } + + Future _getPSTFee(BigInt uploadCost) async { return await implementation .getPstFeePercentage() .then((feePercentage) => @@ -21,4 +34,10 @@ class PstService { .catchError((_) => BigInt.zero, test: (err) => err is UnimplementedError); } + + Future addCommunityTipToTx(Transaction tx) async { + tx.addTag(TipType.tagName, TipType.dataUpload); + tx.setTarget(await getWeightedPstHolder()); + tx.setQuantity(await getPSTFee(tx.reward)); + } } diff --git a/pubspec.yaml b/pubspec.yaml index b87dac1ada..6ecbaa8f18 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: ardrive description: Secure, permanent storage -publish_to: "none" +publish_to: 'none' # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -13,10 +13,10 @@ publish_to: "none" # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.14.1 +version: 1.15.0 environment: - sdk: ">=2.13.0 <3.0.0" + sdk: '>=2.13.0 <3.0.0' dependencies: flutter: