Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PE-7124: save drive key in memory #1915

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 45 additions & 4 deletions lib/blocs/upload/upload_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:ardrive/core/activity_tracker.dart';
import 'package:ardrive/core/upload/domain/repository/upload_repository.dart';
import 'package:ardrive/core/upload/uploader.dart';
import 'package:ardrive/core/upload/view/blocs/upload_manifest_options_bloc.dart';
import 'package:ardrive/entities/constants.dart';
import 'package:ardrive/main.dart';
import 'package:ardrive/manifest/domain/manifest_repository.dart';
import 'package:ardrive/models/forms/cc.dart';
Expand All @@ -21,6 +22,7 @@ import 'package:ardrive/services/config/config_service.dart';
import 'package:ardrive/services/license/license.dart';
import 'package:ardrive/turbo/services/upload_service.dart';
import 'package:ardrive/utils/constants.dart';
import 'package:ardrive/utils/is_custom_manifest.dart';
import 'package:ardrive/utils/logger.dart';
import 'package:ardrive/utils/plausible_event_tracker/plausible_custom_event_properties.dart';
import 'package:ardrive/utils/plausible_event_tracker/plausible_event_tracker.dart';
Expand Down Expand Up @@ -110,6 +112,7 @@ class UploadCubit extends Cubit<UploadState> {
UploadMethod? _manifestUploadMethod;

bool _isManifestsUploadCancelled = false;
bool _isUploadingCustomManifest = false;

void updateManifestSelection(List<ManifestSelection> selections) {
_selectedManifestModels.clear();
Expand All @@ -126,6 +129,11 @@ class UploadCubit extends Cubit<UploadState> {
_manifestUploadMethod = method;
}

void setIsUploadingCustomManifest(bool value) {
_isUploadingCustomManifest = value;
emit((state as UploadReady).copyWith(isUploadingCustomManifest: value));
}

Future<void> prepareManifestUpload() async {
final manifestModels = _selectedManifestModels
.map((e) => UploadManifestModel(
Expand All @@ -150,7 +158,7 @@ class UploadCubit extends Cubit<UploadState> {
fileId: manifestModels[i].existingManifestFileId,
)
.getSingle();

await _createManifestCubit.prepareManifestTx(
manifestName: manifestFileEntry.name,
folderId: manifestFileEntry.parentFolderId,
Expand Down Expand Up @@ -441,6 +449,10 @@ class UploadCubit extends Cubit<UploadState> {
bool showArnsCheckbox = false;

if (_targetDrive.isPublic && _files.length == 1) {
final isACustomManifest = await isCustomManifest(_files.first.ioFile);

logger.d('Is a custom manifest: $isACustomManifest');

emit(
UploadReady(
params: (state as UploadReadyToPrepare).params,
Expand All @@ -461,6 +473,8 @@ class UploadCubit extends Cubit<UploadState> {
arnsRecords: _ants,
showReviewButtonText: false,
selectedManifestSelections: _selectedManifestModels,
isCustomManifest: isACustomManifest,
isUploadingCustomManifest: false,
),
);

Expand Down Expand Up @@ -505,6 +519,8 @@ class UploadCubit extends Cubit<UploadState> {
canShowSettings: showSettings,
showReviewButtonText: false,
selectedManifestSelections: _selectedManifestModels,
isCustomManifest: false, // only applies for single file uploads
isUploadingCustomManifest: false,
),
);
}
Expand Down Expand Up @@ -886,6 +902,10 @@ class UploadCubit extends Cubit<UploadState> {
if (state is UploadReady) {
emit((state as UploadReady).copyWith(arnsRecords: value));
}
}).catchError((e) {
logger.e(
'Error getting ant records for wallet. Proceeding with the upload...',
e);
});

_files
Expand Down Expand Up @@ -1032,9 +1052,15 @@ class UploadCubit extends Cubit<UploadState> {
}

if (manifestFileEntries.isNotEmpty) {
// load arns names
await _arnsRepository
.getAntRecordsForWallet(_auth.currentUser.walletAddress);
try {
// load arns names
await _arnsRepository
.getAntRecordsForWallet(_auth.currentUser.walletAddress);
} catch (e) {
logger.e(
'Error getting ant records for wallet. Proceeding with the upload...',
e);
}
}

emit(
Expand Down Expand Up @@ -1083,6 +1109,21 @@ class UploadCubit extends Cubit<UploadState> {
return;
}

if (_isUploadingCustomManifest) {
final fileWithCustomContentType = await IOFile.fromData(
await _files.first.ioFile.readAsBytes(),
name: _files.first.ioFile.name,
lastModifiedDate: _files.first.ioFile.lastModifiedDate,
contentType: ContentType.manifest,
);

_files.first = UploadFile(
ioFile: fileWithCustomContentType,
parentFolderId: _files.first.parentFolderId,
relativeTo: _files.first.relativeTo,
);
}

_uploadIsInProgress = true;

UploadPlan uploadPlan;
Expand Down
10 changes: 9 additions & 1 deletion lib/blocs/upload/upload_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ class UploadReady extends UploadState {

final bool isArConnect;
final bool showReviewButtonText;

final bool isCustomManifest;
final bool isUploadingCustomManifest;
UploadReady({
required this.paymentInfo,
required this.uploadIsPublic,
Expand All @@ -136,6 +137,8 @@ class UploadReady extends UploadState {
required this.arnsRecords,
required this.showReviewButtonText,
required this.selectedManifestSelections,
required this.isCustomManifest,
required this.isUploadingCustomManifest,
});

// copyWith
Expand All @@ -160,6 +163,8 @@ class UploadReady extends UploadState {
List<ANTRecord>? arnsRecords,
bool? showReviewButtonText,
List<ManifestSelection>? selectedManifestSelections,
bool? isCustomManifest,
bool? isUploadingCustomManifest,
}) {
return UploadReady(
loadingArNSNames: loadingArNSNames ?? this.loadingArNSNames,
Expand All @@ -184,6 +189,9 @@ class UploadReady extends UploadState {
showReviewButtonText: showReviewButtonText ?? this.showReviewButtonText,
selectedManifestSelections:
selectedManifestSelections ?? this.selectedManifestSelections,
isCustomManifest: isCustomManifest ?? this.isCustomManifest,
isUploadingCustomManifest:
isUploadingCustomManifest ?? this.isUploadingCustomManifest,
);
}

Expand Down
18 changes: 16 additions & 2 deletions lib/components/upload_form.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// ignore_for_file: use_build_context_synchronously

import 'dart:async';
import 'dart:math';

Expand Down Expand Up @@ -1977,6 +1975,22 @@ class _UploadReadyWidget extends StatelessWidget {
),
),
],
if (state.isCustomManifest) ...[
const SizedBox(height: 8),
ArDriveCheckBox(
title: 'Convert this file to an Arweave manifest.',
checked: state.isUploadingCustomManifest,
useNewIcons: true,
titleStyle: typography.paragraphNormal(
fontWeight: ArFontWeight.semiBold,
),
onChange: (value) {
context
.read<UploadCubit>()
.setIsUploadingCustomManifest(value);
},
),
],
],
);
},
Expand Down
6 changes: 5 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ late ConfigService configService;
late ArweaveService arweave;
late TurboUploadService _turboUpload;
late PaymentService _turboPayment;
late Database db;

void main() async {
await runZonedGuarded(() async {
Expand Down Expand Up @@ -129,11 +130,14 @@ Future<void> _initializeServices() async {

final config = configService.config;

db = Database();

arweave = ArweaveService(
Arweave(
gatewayUrl: Uri.parse(config.defaultArweaveGatewayForDataRequest.url),
),
ArDriveCrypto(),
db.driveDao,
configService,
);
_turboUpload = config.useTurboUpload
Expand Down Expand Up @@ -395,7 +399,7 @@ class AppState extends State<App> {
),
),
),
RepositoryProvider<Database>(create: (_) => Database()),
RepositoryProvider<Database>(create: (_) => db),
RepositoryProvider<ProfileDao>(
create: (context) => context.read<Database>().profileDao),
RepositoryProvider<DriveDao>(
Expand Down
29 changes: 20 additions & 9 deletions lib/services/arweave/arweave_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:convert';

import 'package:ardrive/core/crypto/crypto.dart';
import 'package:ardrive/entities/entities.dart';
import 'package:ardrive/models/daos/drive_dao/drive_dao.dart';
import 'package:ardrive/services/arweave/arweave_service_exception.dart';
import 'package:ardrive/services/arweave/error/gateway_error.dart';
import 'package:ardrive/services/arweave/get_segmented_transaction_from_drive_strategy.dart';
Expand Down Expand Up @@ -42,12 +43,13 @@ const kMaxNumberOfTransactionsPerPage = 100;
class ArweaveService {
Arweave client;
final ArDriveCrypto _crypto;

final DriveDao _driveDao;
final ArtemisClient _gql;

ArweaveService(
this.client,
this._crypto,
this._driveDao,
ConfigService configService, {
ArtemisClient? artemisClient,
}) : _gql = artemisClient ??
Expand Down Expand Up @@ -588,15 +590,24 @@ class ArweaveService {
continue;
}

final driveKey =
driveTx.getTag(EntityTag.drivePrivacy) == DrivePrivacyTag.private
? await _crypto.deriveDriveKey(
wallet,
driveTx.getTag(EntityTag.driveId)!,
password,
)
: null;
SecretKey? driveKey;

if (driveTx.getTag(EntityTag.drivePrivacy) == DrivePrivacyTag.private) {
driveKey = await _driveDao.getDriveKeyFromMemory(
driveTx.getTag(EntityTag.driveId)!,
);

driveKey ??= await _crypto.deriveDriveKey(
wallet,
driveTx.getTag(EntityTag.driveId)!,
password,
);

_driveDao.putDriveKeyInMemory(
driveID: driveTx.getTag(EntityTag.driveId)!,
driveKey: driveKey,
);
}
try {
final drive = await DriveEntity.fromTransaction(
driveTx,
Expand Down
1 change: 1 addition & 0 deletions lib/sync/domain/repositories/sync_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ class _SyncRepository implements SyncRepository {
//
// It also adds the encryption keys onto the drive models which isn't touched by the
// later system.

final userDriveEntities = await _arweave.getUniqueUserDriveEntities(
wallet,
password,
Expand Down
22 changes: 22 additions & 0 deletions lib/utils/is_custom_manifest.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'dart:convert';

import 'package:ardrive_io/ardrive_io.dart';

/// Checks if a file is an Arweave manifest file by examining its content type and contents.
///
/// Returns true if the file has JSON content type and contains the string "arweave/paths",
/// which indicates it follows the Arweave path manifest specification.
Future<bool> isCustomManifest(IOFile file) async {
if (file.contentType == 'application/json') {
/// Read the first 100 bytes of the file
final first100Bytes = file.openReadStream(0, 100);

await for (var bytes in first100Bytes) {
// verify if file contains "arweave/paths"f
if (utf8.decode(bytes).contains('arweave/paths')) {
return true;
}
}
}
return false;
}
83 changes: 83 additions & 0 deletions test/utils/is_custom_manifest_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:ardrive/utils/is_custom_manifest.dart';
import 'package:ardrive_io/ardrive_io.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';

class MockIOFile extends Mock implements IOFile {}

void main() {
late MockIOFile mockFile;

setUp(() {
mockFile = MockIOFile();
registerFallbackValue(0);
registerFallbackValue(100);
});

group('isCustomManifest', () {
test('returns true when file is JSON and contains arweave/paths', () async {
// Arrange
const jsonContent =
'{"manifest":"arweave/paths","version":"0.1.0","index":{"path":"hello_world.html"},"paths":{"hello_world.html":{"id":"KlwrMWFW9ckVKa8pCGk9a8EjwzYZ7jNVUVHdcE2YkHo"}}}';
final bytes = utf8.encode(jsonContent);

when(() => mockFile.contentType).thenReturn('application/json');
when(() => mockFile.openReadStream(any(), any()))
.thenAnswer((_) => Stream.value(Uint8List.fromList(bytes)));

// Act
final result = await isCustomManifest(mockFile);

// Assert
expect(result, true);
verify(() => mockFile.openReadStream(0, 100)).called(1);
});

test('returns false when file is JSON but does not contain arweave/paths',
() async {
// Arrange
const jsonContent = '{"version": 1, "type": "regular"}';
final bytes = utf8.encode(jsonContent);

when(() => mockFile.contentType).thenReturn('application/json');
when(() => mockFile.openReadStream(any(), any()))
.thenAnswer((_) => Stream.value(Uint8List.fromList(bytes)));

// Act
final result = await isCustomManifest(mockFile);

// Assert
expect(result, false);
verify(() => mockFile.openReadStream(0, 100)).called(1);
});

test('returns false when file is not JSON', () async {
// Arrange
when(() => mockFile.contentType).thenReturn('text/plain');

// Act
final result = await isCustomManifest(mockFile);

// Assert
expect(result, false);
verifyNever(() => mockFile.openReadStream(any(), any()));
});

test('returns false when stream is empty', () async {
// Arrange
when(() => mockFile.contentType).thenReturn('application/json');
when(() => mockFile.openReadStream(any(), any()))
.thenAnswer((_) => Stream.value(Uint8List(0)));

// Act
final result = await isCustomManifest(mockFile);

// Assert
expect(result, false);
verify(() => mockFile.openReadStream(0, 100)).called(1);
});
});
}
Loading