Skip to content

Commit

Permalink
Migrate to Quicknode for ipfs upload (#2337)
Browse files Browse the repository at this point in the history
* feat: upload object to IPFS using quick node

* feat: x api key

* fix: feeback

* fix: code formatting

* feat: added image content types and added some delay on calling blockchain actions

---------

Co-authored-by: Ahsan Ali <[email protected]>
  • Loading branch information
AhsanRns and Ahsan3174 authored Apr 17, 2024
1 parent 946f466 commit a419c10
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 56 deletions.
42 changes: 29 additions & 13 deletions easel/lib/easel_provider.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:io';

import 'package:dartz/dartz.dart';
import 'package:easel_flutter/main.dart';
import 'package:easel_flutter/models/denom.dart';
import 'package:easel_flutter/models/nft.dart';
Expand All @@ -12,10 +13,12 @@ import 'package:easel_flutter/models/upload_progress.dart';
import 'package:easel_flutter/repository/repository.dart';
import 'package:easel_flutter/screens/creator_hub/creator_hub_view_model.dart';
import 'package:easel_flutter/services/third_party_services/audio_player_helper.dart';
import 'package:easel_flutter/services/third_party_services/quick_node.dart';
import 'package:easel_flutter/services/third_party_services/video_player_helper.dart';
import 'package:easel_flutter/utils/constants.dart';
import 'package:easel_flutter/utils/enums.dart';
import 'package:easel_flutter/utils/extension_util.dart';
import 'package:easel_flutter/utils/failure/failure.dart';
import 'package:easel_flutter/utils/file_utils_helper.dart';
import 'package:easel_flutter/widgets/audio_widget.dart';
import 'package:easel_flutter/widgets/loading_with_progress.dart';
Expand Down Expand Up @@ -239,9 +242,7 @@ class EaselProvider extends ChangeNotifier {
royaltyController.text = royalties ?? "";
priceController.text = price ?? "";
noOfEditionController.text = edition ?? "";
_selectedDenom = denom != ""
? Denom.availableDenoms.firstWhere((element) => element.symbol == denom)
: Denom.availableDenoms.first;
_selectedDenom = denom != "" ? Denom.availableDenoms.firstWhere((element) => element.symbol == denom) : Denom.availableDenoms.first;
isFreeDrop = freeDrop!;
notifyListeners();
}
Expand Down Expand Up @@ -571,6 +572,9 @@ class EaselProvider extends ChangeNotifier {
final isCookBookCreated = await createCookbook();

if (isCookBookCreated) {
// this delay is added to wait the transaction is settle
// on the blockchain
Future.delayed(const Duration(milliseconds: 800));
// get device cookbook id
_cookbookId = repository.getCookbookId();
notifyListeners();
Expand Down Expand Up @@ -680,8 +684,7 @@ class EaselProvider extends ChangeNotifier {
currentUsername = sdkResponse.data!.username;
stripeAccountExists = sdkResponse.data!.stripeExists;

supportedDenomList =
Denom.availableDenoms.where((Denom e) => sdkResponse.data!.supportedCoins.contains(e.symbol)).toList();
supportedDenomList = Denom.availableDenoms.where((Denom e) => sdkResponse.data!.supportedCoins.contains(e.symbol)).toList();

if (supportedDenomList.isNotEmpty && selectedDenom.symbol.isEmpty) {
_selectedDenom = supportedDenomList.first;
Expand Down Expand Up @@ -777,8 +780,7 @@ class EaselProvider extends ChangeNotifier {
}
}

bool isThumbnailPresent() =>
nftFormat.format == NFTTypes.audio || nftFormat.format == NFTTypes.video || nftFormat.format == NFTTypes.pdf;
bool isThumbnailPresent() => nftFormat.format == NFTTypes.audio || nftFormat.format == NFTTypes.video || nftFormat.format == NFTTypes.pdf;

Future<bool> saveNftLocally(UploadStep step) async {
final scaffoldMessengerOptionalState = navigatorKey.getState();
Expand Down Expand Up @@ -814,12 +816,26 @@ class EaselProvider extends ChangeNotifier {
uploadThumbnailResponse = uploadResponse.getOrElse(() => StorageResponseModel.initial());
}

final response = await repository.uploadFile(
file: _file!,
onUploadProgressCallback: (value) {
_uploadProgressController.sink.add(value);
},
);
final shouldUploadToQuickNode = QuickNode.listOfQuickNodeAllowedExtension().contains(fileExtension.toLowerCase());

Either<Failure, StorageResponseModel> response;

if (!shouldUploadToQuickNode) {
response = await repository.uploadFile(
file: _file!,
onUploadProgressCallback: (value) {
_uploadProgressController.sink.add(value);
},
);
} else {
response = await repository.uploadFileUsingQuickNode(
uploadIPFSInput: UploadIPFSInput(fileName: fileName, filePath: file!.path, contentType: QuickNode.getContentType(fileExtension)),
onUploadProgressCallback: (value) {
_uploadProgressController.sink.add(value);
},
);
}

if (response.isLeft()) {
loading.dismiss();
LocaleKeys.something_wrong_while_uploading.tr().show();
Expand Down
3 changes: 1 addition & 2 deletions easel/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ bool isTablet = false;

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();

await Firebase.initializeApp();
di.init();
final firebaseCrashlytics = GetIt.I.get<FirebaseCrashlytics>();
Expand Down Expand Up @@ -109,7 +109,6 @@ class MyApp extends StatelessWidget {
}
}


bool _getIsCurrentDeviceTablet() {
final MediaQueryData mediaQuery = MediaQueryData.fromView(PlatformDispatcher.instance.implicitView!);
return mediaQuery.size.shortestSide >= tabletMinWidth;
Expand Down
22 changes: 22 additions & 0 deletions easel/lib/models/storage_response_model.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:easel_flutter/services/third_party_services/quick_node.dart';

class StorageResponseModel {
bool? ok;
Value? value;
Expand All @@ -21,6 +23,26 @@ class StorageResponseModel {
factory StorageResponseModel.initial() {
return StorageResponseModel();
}

factory StorageResponseModel.fromQuickNode({required UploadIPFSOutput uploadIPFSOutput}) {
return StorageResponseModel(
ok: true,
value: Value(
cid: uploadIPFSOutput.pin?.cid,
created: uploadIPFSOutput.created,
size: int.parse(
uploadIPFSOutput.info?.size ?? '0',
),
pin: Pin(
cid: uploadIPFSOutput.pin?.cid,
created: uploadIPFSOutput.created,
size: int.parse(
uploadIPFSOutput.info?.size ?? '0',
),
status: uploadIPFSOutput.status,
)),
);
}
}

class Value {
Expand Down
20 changes: 20 additions & 0 deletions easel/lib/repository/repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:easel_flutter/services/datasources/local_datasource.dart';
import 'package:easel_flutter/services/datasources/remote_datasource.dart';
import 'package:easel_flutter/services/third_party_services/crashlytics_helper.dart';
import 'package:easel_flutter/services/third_party_services/network_info.dart';
import 'package:easel_flutter/services/third_party_services/quick_node.dart';
import 'package:easel_flutter/utils/extension_util.dart';
import 'package:easel_flutter/utils/failure/failure.dart';
import 'package:easel_flutter/utils/file_utils_helper.dart';
Expand Down Expand Up @@ -111,6 +112,11 @@ abstract class Repository {
/// Output : [ApiResponse] the ApiResponse which can contain [success] or [error] response
Future<Either<Failure, StorageResponseModel>> uploadFile({required File file, required OnUploadProgressCallback onUploadProgressCallback});

/// This method is used uploading provided file to the server using [QuickNode]
/// Input : [UploadIPFSInput] which needs to be uploaded
/// Output : [ApiResponse] the ApiResponse which can contain [success] or [error] response
Future<Either<Failure, StorageResponseModel>> uploadFileUsingQuickNode({required UploadIPFSInput uploadIPFSInput, required OnUploadProgressCallback onUploadProgressCallback});

/// This method will get the drafts List from the local database
/// Output: [List] returns that contains a number of [NFT]
Future<Either<Failure, List<NFT>>> getNfts();
Expand Down Expand Up @@ -317,7 +323,21 @@ class RepositoryImp implements Repository {

try {
final storageResponseModel = await remoteDataSource.uploadFile(file: file, uploadProgressCallback: onUploadProgressCallback);
return Right(storageResponseModel);
} on Exception catch (_) {
crashlyticsHelper.recordFatalError(error: _.toString());
return Left(CacheFailure(LocaleKeys.update_failed.tr()));
}
}

@override
Future<Either<Failure, StorageResponseModel>> uploadFileUsingQuickNode({required UploadIPFSInput uploadIPFSInput, required OnUploadProgressCallback onUploadProgressCallback}) async {
if (!await networkInfo.isConnected) {
return Left(NoInternetFailure(LocaleKeys.no_internet.tr()));
}

try {
final storageResponseModel = await remoteDataSource.uploadFileUsingQuickNode(uploadIPFSInput: uploadIPFSInput, onUploadProgressCallback: onUploadProgressCallback);
return Right(storageResponseModel);
} on Exception catch (exception) {
crashlyticsHelper.recordFatalError(error: exception.toString());
Expand Down
3 changes: 3 additions & 0 deletions easel/lib/screens/choose_format_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ class _ChooseFormatScreenState extends State<ChooseFormatScreen> {
return;
}




if (!provider.nftFormat.extensions.contains(result.extension)) {
final fileName = result.fileName.replaceAll(".${result.extension}", "");
errorText.value = LocaleKeys.could_not_uploaded.tr(
Expand Down
40 changes: 9 additions & 31 deletions easel/lib/screens/publish_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,7 @@ class _PublishScreenState extends State<PublishScreen> {
body: Consumer<EaselProvider>(builder: (_, easelProvider, __) {
return Stack(
children: [
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: SizedBox(width: double.infinity, child: buildPreviewWidget(easelProvider))),
Positioned(left: 0, right: 0, top: 0, bottom: 0, child: SizedBox(width: double.infinity, child: buildPreviewWidget(easelProvider))),
Positioned(
left: 10.w,
top: 30.h,
Expand Down Expand Up @@ -187,10 +182,7 @@ class _OwnerBottomDrawerState extends State<OwnerBottomDrawer> {
collapseStatus: viewModel.collapsed,
onCollapsed: (context) => DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [EaselAppTheme.kTransparent, EaselAppTheme.kBlack]),
gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [EaselAppTheme.kTransparent, EaselAppTheme.kBlack]),
),
child: Padding(
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 8.h, bottom: 16.h),
Expand Down Expand Up @@ -236,11 +228,7 @@ class _OwnerBottomDrawerState extends State<OwnerBottomDrawer> {
builder: (_, value, __) {
switch (value) {
case ButtonState.loading:
return SizedBox(
height: 20.h,
width: 15.h,
child: CircularProgressIndicator(
strokeWidth: 2.w, color: EaselAppTheme.kWhite));
return SizedBox(height: 20.h, width: 15.h, child: CircularProgressIndicator(strokeWidth: 2.w, color: EaselAppTheme.kWhite));
case ButtonState.paused:
return InkWell(
onTap: () {
Expand Down Expand Up @@ -282,8 +270,7 @@ class _OwnerBottomDrawerState extends State<OwnerBottomDrawer> {
bufferedBarColor: EaselAppTheme.kLightGrey,
buffered: value.buffered,
total: value.total,
timeLabelTextStyle: TextStyle(
color: EaselAppTheme.kWhite, fontWeight: FontWeight.w800, fontSize: 9.sp),
timeLabelTextStyle: TextStyle(color: EaselAppTheme.kWhite, fontWeight: FontWeight.w800, fontSize: 9.sp),
thumbRadius: 10.h,
timeLabelPadding: 3.h,
onSeek: (position) {
Expand Down Expand Up @@ -316,9 +303,7 @@ class _OwnerBottomDrawerState extends State<OwnerBottomDrawer> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_title(
nft: widget.nft,
owner: widget.nft.type == NftType.TYPE_RECIPE.name ? "you".tr() : widget.nft.creator),
_title(nft: widget.nft, owner: widget.nft.type == NftType.TYPE_RECIPE.name ? "you".tr() : widget.nft.creator),
SizedBox(
height: 30.h,
),
Expand Down Expand Up @@ -387,16 +372,14 @@ class _OwnerBottomDrawerState extends State<OwnerBottomDrawer> {
subtitle: "${widget.nft.tradePercentage}%",
),
SizedBox(height: 5.h),
buildRow(
title: LocaleKeys.content_identifier.tr(), subtitle: widget.nft.cid, canCopy: true),
buildRow(title: LocaleKeys.content_identifier.tr(), subtitle: widget.nft.cid, canCopy: true),
SizedBox(height: 5.h),
CidOrIpfs(
viewCid: (context) {
return const SizedBox.shrink();
},
viewIpfs: (context) {
return buildRow(
title: LocaleKeys.asset_uri.tr(), subtitle: LocaleKeys.view.tr(), viewIPFS: true);
return buildRow(title: LocaleKeys.asset_uri.tr(), subtitle: LocaleKeys.view.tr(), viewIPFS: true);
},
type: widget.nft.assetType,
),
Expand Down Expand Up @@ -522,10 +505,7 @@ class _OwnerBottomDrawerState extends State<OwnerBottomDrawer> {
builder: (_, value, __) {
switch (value) {
case ButtonState.loading:
return SizedBox(
height: 22.h,
width: 22.h,
child: CircularProgressIndicator(strokeWidth: 2.w, color: EaselAppTheme.kWhite));
return SizedBox(height: 22.h, width: 22.h, child: CircularProgressIndicator(strokeWidth: 2.w, color: EaselAppTheme.kWhite));
case ButtonState.paused:
return InkWell(
onTap: () {
Expand Down Expand Up @@ -751,9 +731,7 @@ class BuildPublishBottomSheet extends StatelessWidget {
final WidgetBuilder onOpened;
final bool collapseStatus;

const BuildPublishBottomSheet(
{Key? key, required this.onCollapsed, required this.onOpened, required this.collapseStatus})
: super(key: key);
const BuildPublishBottomSheet({Key? key, required this.onCollapsed, required this.onOpened, required this.collapseStatus}) : super(key: key);

@override
Widget build(BuildContext context) {
Expand Down
22 changes: 19 additions & 3 deletions easel/lib/services/datasources/remote_datasource.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:easel_flutter/easel_provider.dart';
import 'package:easel_flutter/models/storage_response_model.dart';
import 'package:easel_flutter/models/upload_progress.dart';
import 'package:easel_flutter/services/third_party_services/analytics_helper.dart';
import 'package:easel_flutter/services/third_party_services/quick_node.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:pylons_sdk/low_level.dart';

Expand All @@ -21,19 +22,34 @@ abstract class RemoteDataSource {
/// Output : [Future<List<Recipe>>] which will be a Future list of Recipes against the given [cookbookID]
Future<List<Recipe>> getRecipesByCookbookID(String cookBookID);



/// This method is used to log user journey in the easel app.
/// Input: [screenName] the screen name in the easel.
/// Output: [bool] tells whether the user journey is recorded or not
Future<bool> logUserJourney({required String screenName});

/// This method is used uploading provided file to the server using [QuickNode]
/// Input : [UploadIPFSInput] which needs to be uploaded
/// Output : [Future<ApiResponse<StorageResponseModel>>] the ApiResponse which can contain [success] or [error] response
Future<StorageResponseModel> uploadFileUsingQuickNode({required UploadIPFSInput uploadIPFSInput, required OnUploadProgressCallback onUploadProgressCallback});
}

class RemoteDataSourceImpl implements RemoteDataSource {
final Dio httpClient;
final AnalyticsHelper analyticsHelper;
final QuickNode quickNode;

RemoteDataSourceImpl({required this.httpClient, required this.analyticsHelper});
RemoteDataSourceImpl({
required this.httpClient,
required this.analyticsHelper,
required this.quickNode,
});

@override
Future<StorageResponseModel> uploadFileUsingQuickNode({required UploadIPFSInput uploadIPFSInput, required OnUploadProgressCallback onUploadProgressCallback}) async {

final response = await quickNode.uploadNewObjectToIPFS(uploadIPFSInput: uploadIPFSInput, onUploadProgressCallback: onUploadProgressCallback);
return response;
}

@override
Future<StorageResponseModel> uploadFile({required OnUploadProgressCallback uploadProgressCallback, required File file}) async {
Expand Down
Loading

0 comments on commit a419c10

Please sign in to comment.