Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,64 @@ import '../../../../widgets/stack_text_field.dart';
import 'helpers/restore_create_backup.dart';
import 'helpers/swb_file_system.dart';

Future<void> showStackWalletBackupResult({
required BuildContext context,
required String? savedPath,
required Exception? error,
required bool isDesktop,
}) => showDialog<void>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
if (savedPath == null) {
return StackOkDialog(
title: "Backup creation failed",
message: error?.toString() ?? "Unexpected error",
desktopPopRootNavigator: isDesktop,
);
}

if (!isDesktop) {
return StackOkDialog(title: "Backup saved to:", message: savedPath);
}

return DesktopDialog(
maxHeight: double.infinity,
maxWidth: 500,
child: Padding(
padding: const EdgeInsets.only(left: 32, right: 32, bottom: 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 26),
Text(
"${AppConfig.prefix} backup saved to: \n",
style: STextStyles.desktopH3(dialogContext),
),
Text(
savedPath,
style: STextStyles.desktopTextExtraExtraSmall(dialogContext),
),
const SizedBox(height: 40),
Row(
children: [
Expanded(
child: PrimaryButton(
label: "Ok",
buttonHeight: ButtonHeight.l,
onPressed: () => Navigator.of(dialogContext).pop(),
),
),
],
),
],
),
),
);
},
);

class CreateBackupView extends ConsumerStatefulWidget {
const CreateBackupView({super.key});

Expand Down Expand Up @@ -118,70 +176,18 @@ class _RestoreFromFileViewState extends ConsumerState<CreateBackupView> {
);

if (mounted) {
await showStackWalletBackupResult(
context: context,
savedPath: savedPath,
error: ex,
isDesktop: Util.isDesktop,
);
if (savedPath != null) {
await showDialog<dynamic>(
context: context,
barrierDismissible: false,
builder: (_) => !Util.isDesktop
? StackOkDialog(title: "Backup saved to:", message: savedPath)
: DesktopDialog(
maxHeight: double.infinity,
maxWidth: 500,
child: Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
bottom: 32,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 26),
Text(
"${AppConfig.prefix} backup saved to: \n",
style: STextStyles.desktopH3(context),
),
Text(
savedPath,
style: STextStyles.desktopTextExtraExtraSmall(
context,
),
),
const SizedBox(height: 40),
Row(
children: [
Expanded(
child: PrimaryButton(
label: "Ok",
buttonHeight: ButtonHeight.l,
onPressed: Navigator.of(
context,
rootNavigator: true,
).pop,
),
),
],
),
],
),
),
),
);
passwordController.text = "";
passwordRepeatController.text = "";
if (mounted) {
setState(() {});
}
} else {
await showDialog<dynamic>(
context: context,
barrierDismissible: false,
builder: (_) => StackOkDialog(
title: "Backup creation failed",
message: ex?.toString() ?? "Unexpected error",
),
);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import '../../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface
import '../../../../../wallets/wallet/wallet_mixin_interfaces/private_key_interface.dart';
import '../../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../../../wl_gen/interfaces/frost_interface.dart';
import 'swb_view_only_recovery_data.dart';

class PreRestoreState {
final Set<String> walletIds;
Expand Down Expand Up @@ -296,9 +297,27 @@ abstract class SWB {
backupWallet['isFavorite'] = wallet.info.isFavourite;
backupWallet['otherDataJsonString'] = wallet.info.otherDataJsonString;

if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) {
SWBViewOnlyRecoveryData? viewOnlyRecoveryData;
if (wallet is ViewOnlyOptionInterface) {
final rawViewOnlyData = await _secureStore.read(
key: Wallet.getViewOnlyWalletDataSecStoreKey(
walletId: wallet.walletId,
),
);
viewOnlyRecoveryData = normalizeSWBViewOnlyRecoveryData(
operation: "back up",
walletName: wallet.info.name,
walletId: wallet.walletId,
otherData: wallet.info.otherData,
encodedData: rawViewOnlyData,
);
}

if (viewOnlyRecoveryData != null) {
backupWallet['viewOnlyWalletDataKey'] =
(await wallet.getViewOnlyWalletData()).toJsonEncodedString();
viewOnlyRecoveryData.encodedData;
backupWallet['otherDataJsonString'] =
viewOnlyRecoveryData.otherDataJsonString;
} else if (wallet is MnemonicInterface) {
backupWallet['mnemonic'] = await wallet.getMnemonic();
backupWallet['mnemonicPassphrase'] = await wallet
Expand Down Expand Up @@ -399,14 +418,19 @@ abstract class SWB {
String? mnemonic, mnemonicPassphrase, privateKey;

ViewOnlyWalletData? viewOnlyData;
if (info.isViewOnly) {
if (walletbackup['viewOnlyWalletDataKey'] is String) {
final viewOnlyDataEncoded =
walletbackup['viewOnlyWalletDataKey'] as String;

viewOnlyData = ViewOnlyWalletData.fromJsonEncodedString(
viewOnlyDataEncoded,
walletId: info.walletId,
);
} else if (info.isViewOnly) {
throw FormatException(
'Cannot restore view-only wallet "${info.name}": '
'recovery data is missing',
);
} else if (walletbackup['mnemonic'] == null) {
// probably private key based
if (walletbackup['privateKey'] != null) {
Expand Down Expand Up @@ -700,8 +724,34 @@ abstract class SWB {
SecureStorageInterface secureStorageInterface,
ShopInBitService shopinbitService,
) async {
if (!Platform.isLinux) await WakelockPlus.enable();
if (Platform.isLinux) {
return _restoreStackWalletJSON(
jsonBackup,
uiState,
secureStorageInterface,
shopinbitService,
);
}

await WakelockPlus.enable();
try {
return await _restoreStackWalletJSON(
jsonBackup,
uiState,
secureStorageInterface,
shopinbitService,
);
} finally {
await WakelockPlus.disable();
}
}

static Future<bool?> _restoreStackWalletJSON(
String jsonBackup,
StackRestoringUIState? uiState,
SecureStorageInterface secureStorageInterface,
ShopInBitService shopinbitService,
) async {
Logging.instance.d("SWB creating temp backup");
final preRestoreJSON = await createStackWalletJSON(
secureStorage: secureStorageInterface,
Expand All @@ -724,6 +774,7 @@ abstract class SWB {
json.decode(jsonBackup) as Map<String, dynamic>;

final List<dynamic> wallets = validJSON["wallets"] as List;
normalizeSWBViewOnlyWalletBackups(wallets);

// check for duplicate walletIds and assign new ones if required
for (final wallet in wallets) {
Expand Down Expand Up @@ -912,7 +963,6 @@ abstract class SWB {
await status;
}

if (!Platform.isLinux) await WakelockPlus.disable();
// check if cancel was requested and restore previous state
if (_checkShouldCancel(
preRestoreState,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import 'dart:convert';

import '../../../../../models/keys/view_only_wallet_data.dart';
import '../../../../../wallets/isar/models/wallet_info.dart';

class SWBViewOnlyRecoveryData {
final String encodedData;
final Map<String, dynamic> otherData;

const SWBViewOnlyRecoveryData({
required this.encodedData,
required this.otherData,
});

String get otherDataJsonString => jsonEncode(otherData);
}

void normalizeSWBViewOnlyWalletBackups(List<dynamic> wallets) {
for (final entry in wallets) {
if (entry is! Map) {
throw const FormatException("Cannot restore invalid wallet record");
}
final wallet = Map<String, dynamic>.from(entry);
final walletName = wallet["name"];
final walletId = wallet["id"];
if (walletName is! String || walletId is! String) {
throw const FormatException("Cannot restore invalid wallet record");
}

Map<String, dynamic>? otherData;
if (wallet["otherDataJsonString"] is String) {
try {
final decoded = jsonDecode(wallet["otherDataJsonString"] as String);
otherData = Map<String, dynamic>.from(decoded as Map);
} catch (_) {
if (wallet["viewOnlyWalletDataKey"] == null) {
continue;
}
}
}

final normalized = normalizeSWBViewOnlyRecoveryData(
operation: "restore",
walletName: walletName,
walletId: walletId,
otherData: otherData,
encodedData: wallet["viewOnlyWalletDataKey"],
);
if (normalized != null) {
entry["otherDataJsonString"] = normalized.otherDataJsonString;
}
}
}

SWBViewOnlyRecoveryData? normalizeSWBViewOnlyRecoveryData({
required String operation,
required String walletName,
required String walletId,
required Map<String, dynamic>? otherData,
required Object? encodedData,
}) {
final declaresViewOnly = otherData?[WalletInfoKeys.isViewOnlyKey] == true;

if (encodedData == null) {
if (declaresViewOnly) {
throw FormatException(
'Cannot $operation view-only wallet "$walletName": '
'recovery data is missing',
);
}
return null;
}

if (encodedData is! String) {
throw FormatException(
'Cannot $operation view-only wallet "$walletName": '
'recovery data is invalid',
);
}

final ViewOnlyWalletData data;
try {
data = ViewOnlyWalletData.fromJsonEncodedString(
encodedData,
walletId: walletId,
);
} catch (_) {
throw FormatException(
'Cannot $operation view-only wallet "$walletName": '
'recovery data is invalid',
);
}

if (!_hasRequiredRecoveryData(data)) {
throw FormatException(
'Cannot $operation view-only wallet "$walletName": '
'recovery data is incomplete',
);
}

final storedType = otherData?[WalletInfoKeys.viewOnlyTypeIndexKey];
if (storedType != null &&
(storedType is! int || storedType != data.type.index)) {
throw FormatException(
'Cannot $operation view-only wallet "$walletName": '
'recovery metadata conflicts with its data',
);
}

final normalized = Map<String, dynamic>.from(otherData ?? const {})
..[WalletInfoKeys.isViewOnlyKey] = true
..[WalletInfoKeys.viewOnlyTypeIndexKey] = data.type.index;

return SWBViewOnlyRecoveryData(
encodedData: encodedData,
otherData: Map.unmodifiable(normalized),
);
}

bool _hasRequiredRecoveryData(ViewOnlyWalletData data) => switch (data) {
CryptonoteViewOnlyWalletData() =>
data.address.isNotEmpty && data.privateViewKey.isNotEmpty,
AddressViewOnlyWalletData() => data.address.isNotEmpty,
ExtendedKeysViewOnlyWalletData() =>
data.xPubs.isNotEmpty &&
data.xPubs.every((xPub) => xPub.encoded.isNotEmpty),
SparkViewOnlyWalletData() => data.viewKey.isNotEmpty,
};
Loading
Loading