Merge branch 'cake-tech:dev' into dev

This commit is contained in:
BSN ∞/21M
2026-06-25 13:15:14 +02:00
committed by GitHub
14 changed files with 188 additions and 68 deletions
+53 -4
View File
@@ -144,6 +144,21 @@ class BackupMetadata {
}
}
class IncompatibleBackupAppException implements Exception {
IncompatibleBackupAppException({
required this.sourceAppName,
required this.currentAppName,
});
final String sourceAppName;
final String currentAppName;
@override
String toString() {
return 'This backup was created in $sourceAppName and cannot be restored in $currentAppName.';
}
}
class BackupServiceV3 extends $BackupService {
BackupServiceV3(super.secureStorage, super.transactionDescriptionBox, super.keyService, super.sharedPreferences);
@@ -197,7 +212,8 @@ class BackupServiceV3 extends $BackupService {
}
}
Future<void> importBackupFile(File file, String password, {String nonce = secrets.backupSalt}) {
Future<void> importBackupFile(File file, String password,
{String nonce = secrets.backupSalt, bool checkBackupApp = true}) {
final version = getVersionFile(file);
switch (version) {
case BackupVersion.unknown:
@@ -210,11 +226,12 @@ class BackupServiceV3 extends $BackupService {
case BackupVersion.v2:
return super.importBackupV2(file.readAsBytesSync(), password);
case BackupVersion.v3:
return importBackupFileV3(file, password, nonce: nonce);
return importBackupFileV3(file, password, nonce: nonce, checkBackupApp: checkBackupApp);
}
}
Future<void> importBackupFileV3(File file, String password, {String nonce = secrets.backupSalt}) async{
Future<void> importBackupFileV3(File file, String password,
{String nonce = secrets.backupSalt,bool checkBackupApp = true}) async{
// Overall design of v3 backup is the following:
// 1. backup.zip - plaintext zip file that user can open with any archive manager
// 2. backup.zip/README.txt - text file to let user know what is inside of this file
@@ -223,6 +240,11 @@ class BackupServiceV3 extends $BackupService {
final inputStream = InputFileStream(file.path);
final archive = ZipDecoder().decodeStream(inputStream);
if (checkBackupApp) {
await _throwIfBackupWasCreatedInAnotherApp(archive);
}
final metadataFile = archive.findFile('metadata.json');
if (metadataFile == null) {
throw Exception('Invalid v3 backup: missing metadata.json');
@@ -425,7 +447,7 @@ class BackupServiceV3 extends $BackupService {
metadata.sha512sum = (await sha512.bind(dataBinUnencrypted.openRead()).first).toString();
final raf = await dataBinUnencrypted.open();
while (true) {
printV("Reading chunk ${chunkIndex++}");
@@ -491,6 +513,33 @@ This backup was created on ${DateTime.now().toIso8601String()}
return file;
}
Future<void> _throwIfBackupWasCreatedInAnotherApp(Archive archive) async {
final readmeFile = archive.findFile('README.txt');
if (readmeFile == null) return;
final readmeBytes = readmeFile.rawContent?.readBytes();
if (readmeBytes == null) return;
final readmeString = utf8.decode(readmeBytes, allowMalformed: true);
final sourceAppName = _extractBackupAppName(readmeString);
if (sourceAppName == null) return;
final currentAppName = (await PackageInfo.fromPlatform()).appName;
if (sourceAppName == currentAppName) return;
throw IncompatibleBackupAppException(
sourceAppName: sourceAppName,
currentAppName: currentAppName,
);
}
String? _extractBackupAppName(String readme) {
final match = RegExp(r'^This is a (.+) backup\.', multiLine: true).firstMatch(readme);
return match?.group(1)?.trim();
}
static const chunkSize = 24 * 1024 * 1024; // 24MiB
File setVersionFile(File file, BackupVersion version) {
@@ -280,8 +280,8 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
final trade = Trade(
id: depositAddress,
// Using deposit address as trade ID
from: from,
to: to,
from: request.fromCurrency,
to: request.toCurrency,
provider: description,
providerName: title,
state: TradeState.created,
+30 -20
View File
@@ -1,6 +1,7 @@
import 'package:cake_wallet/core/utilities.dart';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/payjoin_copy_modal.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_address_type.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_address_widget.dart';
@@ -22,6 +23,7 @@ import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/payment_uris.dart';
import 'package:cw_core/receive_page_option.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
@@ -194,29 +196,37 @@ class _NewReceivePageState extends State<NewReceivePage> {
ModalTopBar(
title: _largeQrMode ? "" : S.of(context).receive,
leadingIcon: Icon(Icons.close),
trailingIcon: _largeQrMode
? Icon(Icons.share)
: widget.addressListViewModel.hasAddressRotation
/* TODO rotating is broken on mweb, disabling for now, fix after mvp*/
&&
!(widget.receiveOptionViewModel.selectedReceiveOption.description ?? "")
.toLowerCase()
.contains("mweb")
? Icon(Icons.refresh)
: null,
onLeadingPressed: () {
Navigator.of(context, rootNavigator: true).pop();
},
onTrailingPressed: () {
if (_largeQrMode) {
ShareUtil.share(
text: widget.addressListViewModel.uri.toString(),
context: context,
);
} else if (widget.addressListViewModel.hasAddressRotation) {
widget.addressListViewModel.rotateAddress();
}
},
trailingWidget: Observer(
builder: (_) => AnimatedSwitcher(
duration: Duration(milliseconds: 300),
child: _largeQrMode || widget.addressListViewModel.hasAddressRotation
/* TODO rotating is broken on mweb, disabling for now, fix after mvp*/
&&
!(widget.receiveOptionViewModel.selectedReceiveOption.description ?? "")
.toLowerCase()
.contains("mweb") ? ModernButton(
key: ValueKey(_largeQrMode),
size: 36,
icon: _largeQrMode ? Icon(Icons.share) : widget.addressListViewModel.isRotatingAddress
? CupertinoActivityIndicator()
: Icon(Icons.refresh),
onPressed: () {
if(_largeQrMode) {
ShareUtil.share(
text: widget.addressListViewModel.uri.toString(),
context: context,
);
} else {
if(widget.addressListViewModel.hasAddressRotation) {
widget.addressListViewModel.rotateAddress();
}
}
}):SizedBox.shrink(),
),
),
),
Expanded(
child: Column(
@@ -101,9 +101,6 @@ class HistorySection extends StatelessWidget {
final trade = item.trade;
final tradeFrom = trade.from;
final tradeTo = trade.to;
if (tradeFrom == null || tradeTo == null) {
return const SizedBox.shrink();
}
return GestureDetector(
onTap: () => Navigator.of(context)
@@ -15,13 +15,13 @@ class HistoryTradeTile extends StatelessWidget {
required this.roundedTop,
required this.roundedBottom,
required this.bottomSeparator,
required this.from,
required this.to,
this.from,
this.to,
required this.swapState,
required this.provider});
final CryptoCurrency from;
final CryptoCurrency to;
final CryptoCurrency? from;
final CryptoCurrency? to;
final ExchangeProviderDescription provider;
final String date;
final String amount;
@@ -32,6 +32,11 @@ class HistoryTradeTile extends StatelessWidget {
final TradeState swapState;
Widget _getLeadingStack(BuildContext context) {
if (from == null || to == null) {
return SizedBox(width: 50, height: 50);
}
double currencyIconSize = 22.0;
return SizedBox(
@@ -40,7 +45,7 @@ class HistoryTradeTile extends StatelessWidget {
child: Stack(
children: [
CakeImageWidget(
imageUrl: _getIconPath(from), width: currencyIconSize, height: currencyIconSize),
imageUrl: _getIconPath(from!), width: currencyIconSize, height: currencyIconSize),
Positioned(
top: currencyIconSize / 2,
left: currencyIconSize / 2,
@@ -50,7 +55,7 @@ class HistoryTradeTile extends StatelessWidget {
Border.all(width: 2, color: Theme.of(context).colorScheme.surfaceContainer),
shape: BoxShape.circle),
child: CakeImageWidget(
imageUrl: _getIconPath(to),
imageUrl: _getIconPath(to!),
width: currencyIconSize,
height: currencyIconSize,
),
@@ -85,8 +90,8 @@ class HistoryTradeTile extends StatelessWidget {
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500)),
Text(from.title, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)),
if (fromChainIcon.isNotEmpty)
if(from?.title != null) Text(from!.title, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)),
if (fromChainIcon?.isNotEmpty ?? false)
CakeImageWidget(
imageUrl: fromChainIcon,
width: 12,
@@ -106,10 +111,11 @@ class HistoryTradeTile extends StatelessWidget {
receiveAmount.withMaxDecimals(8),
style: TextStyle(fontWeight: FontWeight.w500),
),
if(to!=null)
Text(
to.title,
to!.title,
),
if (toChainIcon.isNotEmpty) CakeImageWidget(imageUrl: toChainIcon, width: 12, height: 12)
if (toChainIcon?.isNotEmpty ?? false) CakeImageWidget(imageUrl: toChainIcon, width: 12, height: 12)
],
),
leadingIcon: _getLeadingStack(context),
@@ -137,7 +143,8 @@ class HistoryTradeTile extends StatelessWidget {
return "";
}
String _getChainIcon(CryptoCurrency currency) {
String? _getChainIcon(CryptoCurrency? currency) {
if(currency == null) return null;
try {
if (currency.title.isNotEmpty) {
final parsedCurrency = CryptoCurrency.safeParseCurrencyFromString(currency.title, tag: currency.tag);
@@ -10,6 +10,7 @@ import 'package:cake_wallet/new-ui/pages/send_page.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
import 'package:cw_core/card_design.dart';
@@ -72,8 +73,6 @@ class _CardsViewState extends State<CardsView> {
final top = baseTop - (howFarBehind * overlapAmount);
final double cardWidth = MediaQuery.of(context).size.width * 0.878;
final double effectiveCardWidth = min(cardWidth, 768);
final left = (parentWidth - effectiveCardWidth) / 2.0;
@@ -228,9 +227,9 @@ class _CardsViewState extends State<CardsView> {
}
}
double get effectiveCardWidth => min(MediaQuery.of(context).size.width * 0.878, responsiveLayoutUtil.shouldRenderMobileUI ? 768 : 512);
double _getBoxHeight(int numCards, double overlapAmount) {
final double cardWidth = MediaQuery.of(context).size.width * 0.878;
final double effectiveCardWidth = min(cardWidth, 768);
return
/* height of initial card */
(2 / 3.2) * (effectiveCardWidth) +
@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
@@ -29,7 +31,7 @@ class ReceiveQrCode extends StatelessWidget {
@override
Widget build(BuildContext context) {
final double targetY = largeQrMode ? largeQrModeBottomPadding+50 : 0;
final double resolvedSize = MediaQuery.of(context).size.width * 0.5;
final double resolvedSize = min(MediaQuery.of(context).size.width, MediaQuery.of(context).size.height) * 0.5;
final double resolvedScale = largeQrMode ? 1.7 : 1;
return Stack(
+6 -2
View File
@@ -154,8 +154,12 @@ class BackupPage extends BasePage {
leftButtonText: S.of(context).share,
actionRightButton: () async {
await backupViewModelBase.saveToDownload(backup.name, backup.file);
Navigator.of(dialogContext).pop();
await showBar<void>(context, S.of(context).file_saved);
if (dialogContext.mounted && Navigator.canPop(dialogContext)) {
Navigator.of(dialogContext).pop();
}
if (context.mounted && Navigator.canPop(context)) {
await showBar<void>(context, S.of(context).file_saved);
}
},
actionLeftButton: () async {
Navigator.of(dialogContext).pop();
@@ -9,6 +9,7 @@ import 'package:cw_core/root_dir.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:path/path.dart' as path;
class DevMoneroWalletCacheDebugPage extends StatelessWidget {
@@ -263,6 +264,7 @@ class _JsonExplorerState extends State<JsonExplorer> {
),
)
: ListView.builder(
controller: ModalScrollController.of(context),
itemCount: _filteredItems.length,
itemBuilder: (context, index) {
final item = _filteredItems[index];
@@ -1,6 +1,8 @@
import 'package:cake_wallet/core/backup_service_v3.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
@@ -18,6 +20,7 @@ class RestoreFromBackupPage extends BasePage {
final RestoreFromBackupViewModel restoreFromBackupViewModel;
final TextEditingController textEditingController;
bool _isStateReactionSet = false;
@override
String get title => S.current.restore_title_from_backup;
@@ -32,22 +35,26 @@ class RestoreFromBackupPage extends BasePage {
@override
Widget body(BuildContext context) {
reaction((_) => restoreFromBackupViewModel.state, (ExecutionState state) {
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: state.error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
});
});
}
});
if (!_isStateReactionSet) {
_isStateReactionSet = true;
reaction((_) => restoreFromBackupViewModel.state, (ExecutionState state) {
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: state.error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
});
});
}
});
}
return Center(
child: ConstrainedBox(
@@ -175,6 +182,29 @@ class RestoreFromBackupPage extends BasePage {
}
try {
await restoreFromBackupViewModel.import(textEditingController.text);
} on IncompatibleBackupAppException catch (e) {
final isConfirmed = await showPopUp<bool>(
context: context,
builder: (_) {
return AlertWithTwoActions(
alertTitle: S.current.warning,
alertContent:
'This backup was created in ${e.sourceAppName}, but you are restoring it in ${e.currentAppName}. '
'Please make sure this is the correct backup file before continuing.',
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).ok,
actionLeftButton: () => Navigator.of(context).pop(false),
actionRightButton: () => Navigator.of(context).pop(true),
);
},
);
if (isConfirmed ?? false) {
await restoreFromBackupViewModel.import(
textEditingController.text,
checkBackupApp: false,
);
}
} catch (e) {
await showPopUp<void>(
context: context,
@@ -1242,6 +1242,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
trade.walletId = wallet.id;
trade.chainId = wallet.chainId;
trade.fromWalletAddress = wallet.walletAddresses.address;
if(trade.from == null) {
trade.from = depositCurrency;
}
if(trade.to == null) {
trade.to = receiveCurrency;
}
final canCreateTrade = await isCanCreateTrade(trade);
if (!canCreateTrade.result) {
@@ -161,7 +161,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
allowChangeWallet: true,
isReconnect: true,
onConnectDevice: (context, ledgerVM) async {
if (context.mounted) {
if (context.mounted && Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
@@ -32,7 +32,7 @@ abstract class RestoreFromBackupViewModelBase with Store {
void reset() => filePath = '';
@action
Future<void> import(String password) async {
Future<void> import(String password,{bool checkBackupApp = true}) async {
try {
state = IsExecutingState();
@@ -44,7 +44,9 @@ abstract class RestoreFromBackupViewModelBase with Store {
final file = File(filePath);
try {
await backupService.importBackupFile(file, password);
await backupService.importBackupFile(file, password, checkBackupApp: checkBackupApp);
} on IncompatibleBackupAppException {
rethrow;
} catch (e, s) {
if (e.toString().contains("unknown_backup_version")) {
state = FailureState('This is not a valid backup file, please make sure you have selected the correct one');
@@ -74,6 +76,9 @@ abstract class RestoreFromBackupViewModelBase with Store {
});
state = ExecutedSuccessfullyState();
} on IncompatibleBackupAppException {
state = InitialExecutionState();
rethrow;
} catch (e, s) {
var msg = e.toString().toLowerCase();
@@ -614,11 +614,20 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
void setAddress(WalletAddressListItem address) =>
wallet.walletAddresses.address = address.address;
@observable
bool isRotatingAddress = false;
@action
Future<void> rotateAddress() async {
await createNewAddress(wallet, "");
if (isElectrumWallet) {
if(isRotatingAddress) {
return;
}
try {
isRotatingAddress = true;
await createNewAddress(wallet, "");
wallet.walletAddresses.address = addressList.whereType<WalletAddressListItem>().last.address;
} finally {
isRotatingAddress = false;
}
}