Merge branch 'staging' into fix/305-multiline-transaction-notes
What changed, and why it matters
This commit is a large merge that mainly removes old integration tests and adds new desktop 'forgot password' reset tests. It also adds a safe-shutdown path for background Firo cache workers and databases. The changes look like defensive hardening for a data-wipe feature rather than an active security vulnerability. There is no clear exploit or malicious change in the diff, but the merge is broad and the actual production reset logic is only partially visible, so we cannot fully verify it is safe.
Treat as a hardening/test refactor commit. Review the full production reset implementation (desktop_password_service, desktop_startup_and_reset, route_generator changes referenced in file list) to confirm the data deletion order matches the test expectations and that no secrets remain accessible after reset. Run the new integration tests on all desktop platforms.
Security signals we found
Added safe shutdown of Firo cache isolates/SQLite databases before reset exit
New integration tests verify desktop forgot-password reset deletes secrets and preserves backups
Test harness intercepts exit() and IOOverrides to observe reset side effects
Large deletion of legacy integration tests reduces attack surface in test code
Merge title references unrelated multiline transaction notes fix; actual diff does not show it
Evidence from the diff
The merge deletes the legacy integration_test/ robot suite and introduces a new desktop_forgot_password integration-test harness that exercises app reset flows against a throwaway data directory. Production code changes are limited: FiroCacheCoordinator.close() and _FiroCacheWorker.close() now cleanly terminate isolates and close SQLite databases before process exit. The harness intercepts exit(), IOOverrides, and path_provider to verify that reset deletes sensitive files (hive/desktopdata.hive, isar/desktopStore.isar, wallet files) while preserving backups and tor state. No cryptographic, network, or input-validation flaws are visible in the supplied diff. The commit title references ‘fix/305-multiline-transaction-notes’, but the diff does not show any related UI fix, suggesting the merge absorbed unrelated staging work.
Changed components
lib/db/sqlite/firo_cache_coordinator.dartlib/db/sqlite/firo_cache_worker.dartintegration_test/desktop_forgot_password/*analysis_options.yamlInspect captured patch +2352 / −3190
### analysis_options.yaml
@@ -13,8 +13,6 @@ analyzer:
- "**/*.g.dart"
- "**/*.freezed.dart"
- "test/.test_coverage.dart"
- - "integrated_test/**"
- - "integration_test/**.dart"
- "crypto_plugins/**"
- "bin/cache/**"
- "lib/generated_plugin_registrant.dart"
### integration_test/bot_runners/create_wallet_until_pin_confirmation.dart
@@ -1,46 +0,0 @@
-import '../bots/onboarding/create_pin_view_bot.dart';
-import '../bots/onboarding/name_your_wallet_view_bot.dart';
-import '../bots/onboarding/terms_and_conditions_bot.dart';
-
-Future<void> createWalletUntilPinConfirmation(
- TermsAndConditionsViewBot termsAndConditionsViewBot,
- NameYourWalletViewBot nameYourWalletViewBot,
- CreatePinViewBot createPinViewBot,
-) async {
- await termsAndConditionsViewBot.ensureVisible();
-
- // test scrolling of terms & conditions
- await termsAndConditionsViewBot.scrollDown();
- await termsAndConditionsViewBot.scrollUp();
-
- // accept terms & conditions
- await termsAndConditionsViewBot.tapIAccept();
-
- await nameYourWalletViewBot.ensureVisible();
- // test tap back
- await nameYourWalletViewBot.tapBack();
- await termsAndConditionsViewBot.ensureVisible();
- // accept terms & conditions again
- await termsAndConditionsViewBot.tapIAccept();
- await nameYourWalletViewBot.ensureVisible();
-
- // enter a wallet name
- await nameYourWalletViewBot.enterWalletName("My Firo Wallet");
-
- // tap next
- await nameYourWalletViewBot.tapNext();
-
- await createPinViewBot.ensureVisible();
- // tap back
- await createPinViewBot.tapBack();
- await nameYourWalletViewBot.ensureVisible();
-
- // tap next
- await nameYourWalletViewBot.tapNext();
- await createPinViewBot.ensureVisible();
-
- // enter a pin
- await createPinViewBot.enterPin();
- // confirm pin
- await createPinViewBot.confirmMatchedPin();
-}
### integration_test/bots/addressbook/add_address_book_entry_view_bot.dart
@@ -1,43 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/address_book_view/subviews/add_address_book_entry_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
-import 'package:stackwallet/widgets/custom_buttons/gradient_button.dart';
-import 'package:stackwallet/widgets/custom_buttons/simple_button.dart';
-
-class AddAddressBookEntryViewBot {
- final WidgetTester tester;
-
- const AddAddressBookEntryViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(AddAddressBookEntryView));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byType(AppBarIconButton));
- await tester.pumpAndSettle();
- }
-
- Future<void> enterAddress(String text) async {
- await tester.enterText(
- find.byKey(Key("addAddressBookEntryViewAddressField")), text);
- await tester.pumpAndSettle();
- }
-
- Future<void> enterName(String text) async {
- await tester.enterText(
- find.byKey(Key("addAddressBookEntryViewNameField")), text);
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCancel() async {
- await tester.tap(find.byType(SimpleButton));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapSave() async {
- await tester.tap(find.byType(GradientButton));
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/addressbook/address_book_card_bot.dart
@@ -1,35 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/widgets/address_book_card.dart';
-
-class AddressBookCardBot {
- final WidgetTester tester;
-
- const AddressBookCardBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(AddressBookCard));
- }
-
- Future<void> toggleExpandCard() async {
- await tester.tap(find.byType(AddressBookCard));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapDetails() async {
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is SubButton && widget.label == "DETAILS"));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCopy() async {
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is SubButton && widget.label == "COPY"));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapSend() async {
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is SubButton && widget.label == "SEND FIRO"));
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/addressbook/address_book_entry_details_view_bot.dart
@@ -1,59 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/address_book_view/subviews/address_book_entry_details_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/gradient_button.dart';
-
-class AddressBookEntryDetailsViewBot {
- final WidgetTester tester;
-
- const AddressBookEntryDetailsViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(AddressBookEntryDetailsView));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byKey(Key("addressBookDetailsBackButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapMore() async {
- await tester.tap(find.byKey(Key("addressBookDetailsDeleteButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapDelete() async {
- await tester
- .tap(find.byKey(Key("addressBookDetailsContextMenuDeleteButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapConfirmDelete() async {
- await tester
- .tap(find.byKey(Key("deleteContactConfirmationDialogDeleteButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCancelDelete() async {
- await tester
- .tap(find.byKey(Key("deleteContactConfirmationDialogCancelButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCopyAddress() async {
- await tester
- .tap(find.byKey(Key("addressBookEntryDetailsCopyAddressButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapEdit() async {
- await tester
- .tap(find.byKey(Key("addressBookEntryDetailsEditEntryButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapSend() async {
- await tester.tap(find.byType(GradientButton));
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/addressbook/address_book_view_bot.dart
@@ -1,28 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/address_book_view/address_book_view.dart';
-
-class AddressBookViewBot {
- final WidgetTester tester;
-
- const AddressBookViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(AddressBookView));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byKey(Key("addressBookBackButton")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapAdd() async {
- await tester.tap(find.byKey(Key("addressBookAddButton")));
- await tester.pumpAndSettle();
- }
-
- Future<void> enterQuery(String query) async {
- await tester.enterText(find.byType(TextField), query);
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/addressbook/edit_address_book_entry_view_bot.dart
@@ -1,40 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/address_book_view/subviews/edit_address_book_entry_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/gradient_button.dart';
-import 'package:stackwallet/widgets/custom_buttons/simple_button.dart';
-
-class EditAddressBookEntryViewBot {
- final WidgetTester tester;
-
- const EditAddressBookEntryViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(EditAddressBookEntryView));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byKey(Key("editAddressBookEntryBackButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCancel() async {
- await tester.tap(find.byType(SimpleButton));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapSave() async {
- await tester.tap(find.byType(GradientButton));
- await tester.pumpAndSettle();
- }
-
- Future<void> enterAddress(String text) async {
- await tester.enterText(
- find.byKey(Key("editAddressBookEntryAddressFieldKey")), text);
- }
-
- Future<void> enterName(String text) async {
- await tester.enterText(
- find.byKey(Key("editAddressBookEntryNameFieldKey")), text);
- }
-}
### integration_test/bots/lockscreen_view_bot.dart
@@ -1,30 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/lockscreen_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
-import 'package:stackwallet/widgets/custom_pin_put/pin_keyboard.dart';
-
-class LockscreenViewBot {
- final WidgetTester tester;
-
- const LockscreenViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(LockscreenView));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byType(AppBarIconButton));
- await tester.pumpAndSettle();
- }
-
- Future<void> enterPin(String pin) async {
- for (int i = 0; i < pin.length - 1; i++) {
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == pin[i]));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- }
- await tester.tap(find.byWidgetPredicate((widget) =>
- widget is NumberKey && widget.number == pin[pin.length - 1]));
- await tester.pumpAndSettle(Duration(seconds: 1));
- }
-}
### integration_test/bots/main_view_bot.dart
@@ -1,38 +0,0 @@
-import 'package:flutter/cupertino.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/main_view.dart';
-
-class MainViewBot {
- final WidgetTester tester;
-
- const MainViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(MainView));
- }
-
- Future<void> tapRefresh() async {
- await tester.tap(find.byKey(Key("mainViewRefreshButton")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapSettings() async {
- await tester.tap(find.byKey(Key("mainViewSettingsButton")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapSend() async {
- await tester.tap(find.byKey(Key("mainViewNavBarSendItemKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapWallet() async {
- await tester.tap(find.byKey(Key("mainViewNavBarWalletItemKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapReceive() async {
- await tester.tap(find.byKey(Key("mainViewNavBarReceiveItemKey")));
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/onboarding/backup_key_view_bot.dart
@@ -1,65 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/onboarding_view/backup_key_view.dart';
-import 'package:stackwallet/utilities/misc_global_constants.dart';
-import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
-import 'package:stackwallet/widgets/custom_buttons/gradient_button.dart';
-
-class BackupKeyViewBot {
- final WidgetTester tester;
-
- const BackupKeyViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(BackupKeyView));
- }
-
- Future<void> tapVerify() async {
- final buttonFinder = find.byType(GradientButton);
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-
- Future<void> tapQrCode() async {
- final buttonFinder = find.byKey(Key("backupKeyQrCodeButtonKey"));
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-
- Future<void> tapSkip() async {
- final buttonFinder = find.byType(TextButton);
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCopy() async {
- final buttonFinder = find.byKey(Key("backupKeyViewCopyButtonKey"));
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-
- Future<void> tapBack() async {
- final buttonFinder = find.byType(AppBarIconButton);
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-
- Future<List<String?>> displayedMnemonic() async {
- List<String?> words = [];
- for (int i = 0; i < CampfireConstants.seedPhraseWordCount; i++) {
- final text = find
- .byKey(Key("mnemonic_word_text_$i"))
- .evaluate()
- .single
- .widget as Text;
-
- words.add(text.data);
- }
- return words;
- }
-}
### integration_test/bots/onboarding/backup_key_warning_bot.dart
@@ -1,43 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/onboarding_view/backup_key_warning_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
-import 'package:stackwallet/widgets/custom_buttons/gradient_button.dart';
-
-class BackupKeyWarningViewBot {
- final WidgetTester tester;
-
- const BackupKeyWarningViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(BackupKeyWarningView));
- }
-
- Future<void> tapViewBackupKey() async {
- final buttonFinder = find.byType(GradientButton);
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCheckBox() async {
- final buttonFinder = find.byType(Checkbox);
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-
- Future<void> tapSkip() async {
- final buttonFinder = find.byType(TextButton);
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-
- Future<void> tapBack() async {
- final buttonFinder = find.byType(AppBarIconButton);
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/onboarding/create_pin_view_bot.dart
@@ -1,66 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/onboarding_view/create_pin_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
-import 'package:stackwallet/widgets/custom_pin_put/pin_keyboard.dart';
-
-class CreatePinViewBot {
- final WidgetTester tester;
-
- const CreatePinViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(CreatePinView));
- }
-
- Future<void> enterPin() async {
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "1"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "2"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "3"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "4"));
- await tester.pumpAndSettle(Duration(seconds: 1));
- }
-
- Future<void> confirmMatchedPin() async {
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "1"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "2"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "3"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "4"));
- await tester.pump(Duration(seconds: 1));
- }
-
- Future<void> confirmUnmatchedPin() async {
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "7"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "9"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "8"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "5"));
- await tester.pump(Duration(seconds: 1));
- }
-
- Future<void> tapBack() async {
- final buttonFinder = find.byType(AppBarIconButton);
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/onboarding/name_your_wallet_view_bot.dart
@@ -1,36 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/onboarding_view/name_your_wallet_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
-import 'package:stackwallet/widgets/custom_buttons/gradient_button.dart';
-
-class NameYourWalletViewBot {
- final WidgetTester tester;
-
- const NameYourWalletViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(NameYourWalletView));
- }
-
- Future<void> enterWalletName(String name) async {
- final textFieldFinder = find.byType(TextField);
- await tester.ensureVisible(textFieldFinder);
- await tester.enterText(textFieldFinder, name);
- await tester.pumpAndSettle();
- }
-
- Future<void> tapNext() async {
- final buttonFinder = find.byType(GradientButton);
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-
- Future<void> tapBack() async {
- final buttonFinder = find.byType(AppBarIconButton);
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/onboarding/onboarding_view_bot.dart
@@ -1,28 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/onboarding_view/onboarding_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/gradient_button.dart';
-import 'package:stackwallet/widgets/custom_buttons/simple_button.dart';
-
-class OnboardingViewBot {
- final WidgetTester tester;
-
- const OnboardingViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(OnboardingView));
- }
-
- Future<void> tapCreateNewWallet() async {
- final buttonFinder = find.byType(GradientButton);
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-
- Future<void> tapRestoreWallet() async {
- final buttonFinder = find.byType(SimpleButton);
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/onboarding/restore_wallet_form_view_bot.dart
@@ -1,59 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/onboarding_view/restore_wallet_form_view.dart';
-
-class RestoreWalletFormViewBot {
- final WidgetTester tester;
-
- const RestoreWalletFormViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(RestoreWalletFormView));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byKey(Key("onboardingAppBarBackButton")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapScanQrCode() async {
- await tester.tap(find.byKey(Key("restoreWalletViewScanQRButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCancelScanQrCode() async {
- await tester.tap(find.text("CANCEL"));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapPaste() async {
- await tester.tap(find.byKey(Key("restoreWalletViewPasteButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapRestore(bool settle) async {
- await tester.tap(find.byKey(Key("restoreMnemonicViewRestoreButtonKey")));
- if (settle) {
- await tester.pumpAndSettle();
- } else {
- await tester.pump(Duration(seconds: 1));
- }
- }
-
- Future<void> tapCancelRestore() async {
- await tester
- .tap(find.byKey(Key("restoreWalletWaitingDialogCancelButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> enterWord(String word, int position) async {
- await tester.enterText(
- find.byKey(Key("restoreMnemonicFormField_$position")), word);
- }
-
- Future<void> scrollDown() async {
- await tester.fling(
- find.byType(SingleChildScrollView), Offset(0, -500), 10000);
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/onboarding/terms_and_conditions_bot.dart
@@ -1,41 +0,0 @@
-import 'package:flutter/widgets.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/onboarding_view/terms_and_conditions_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
-import 'package:stackwallet/widgets/custom_buttons/gradient_button.dart';
-
-class TermsAndConditionsViewBot {
- final WidgetTester tester;
-
- const TermsAndConditionsViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(TermsAndConditionsView));
- }
-
- Future<void> tapIAccept() async {
- final buttonFinder = find.byType(GradientButton);
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-
- Future<void> tapBack() async {
- final buttonFinder = find.byType(AppBarIconButton);
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-
- Future<void> scrollDown() async {
- await tester.fling(
- find.byType(SingleChildScrollView), Offset(0, -1000), 10000);
- await tester.pumpAndSettle();
- }
-
- Future<void> scrollUp() async {
- await tester.fling(
- find.byType(SingleChildScrollView), Offset(0, 1000), 10000);
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/onboarding/verify_backup_key_view_bot.dart
@@ -1,56 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/onboarding_view/verify_backup_key_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
-import 'package:stackwallet/widgets/custom_buttons/gradient_button.dart';
-
-class VerifyBackupKeyViewBot {
- final WidgetTester tester;
-
- const VerifyBackupKeyViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(VerifyBackupKeyView));
- }
-
- Future<void> tapConfirm() async {
- await tester.fling(
- find.byType(SingleChildScrollView), Offset(0, -500), 1000);
- await tester.pumpAndSettle();
- final buttonFinder = find.byType(GradientButton);
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-
- Future<void> tapBack() async {
- final buttonFinder = find.byType(AppBarIconButton);
- await tester.ensureVisible(buttonFinder);
- await tester.tap(buttonFinder);
- await tester.pumpAndSettle();
- }
-
- Future<void> enterRequestedWord(List<String?> words) async {
- String text = (find
- .byKey(Key("nThWordVerificationString"))
- .evaluate()
- .single
- .widget as Text)
- .data!;
-
- int? index;
-
- if (text.length == 33) {
- index = int.parse(text.substring(9, 11));
- } else if (text.length == 32) {
- index = int.parse(text.substring(9, 10));
- }
-
- if (index == null) {
- throw Exception("Backup key verification text string failed test");
- }
-
- await tester.enterText(find.byType(TextField), words[index - 1]!);
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/receive_view_bot.dart
@@ -1,22 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/wallet_view/receive_view.dart';
-
-class ReceiveViewBot {
- final WidgetTester tester;
-
- const ReceiveViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(ReceiveView));
- }
-//
-// Future<void> tapX() async {
-// await tester.tap(find.byKey(Key("cancelTransactionSearchAppBarButton")));
-// await tester.pumpAndSettle();
-// }
-//
-// Future<void> tapCancel() async {
-// await tester.tap(find.byType(SimpleButton));
-// await tester.pumpAndSettle();
-// }
-}
### integration_test/bots/rescan_warning_view_bot.dart
@@ -1,56 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/gradient_button.dart';
-
-class RescanWarningViewBot {
- final WidgetTester tester;
-
- const RescanWarningViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(RescanWarningView));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byKey(Key("settingsAppBarBackButton")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapQrCode() async {
- await tester.tap(find.byKey(Key("rescanWarningShowQrCodeButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCancelQrCode() async {
- await tester
- .tap(find.byKey(Key("rescanWarningQrCodePopupCancelButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCopy() async {
- await tester.tap(find.byKey(Key("rescanWarningCopySeedButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapContinue() async {
- await tester.tap(find.byType(GradientButton));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapConfirmContinue() async {
- await tester.tap(find.byKey(Key("rescanWarningContinueRescanButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCancelContinue() async {
- await tester.tap(find.byKey(Key("rescanWarningContinueCancelButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapOkOnRescanFailedDialog() async {
- await tester
- .tap(find.byKey(Key("rescanWarningViewRescanFailedOkButtonKey")));
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/send_view_bot.dart
@@ -1,22 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/wallet_view/send_view.dart';
-
-class SendViewBot {
- final WidgetTester tester;
-
- const SendViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(SendView));
- }
- //
- // Future<void> tapX() async {
- // await tester.tap(find.byKey(Key("cancelTransactionSearchAppBarButton")));
- // await tester.pumpAndSettle();
- // }
- //
- // Future<void> tapCancel() async {
- // await tester.tap(find.byType(SimpleButton));
- // await tester.pumpAndSettle();
- // }
-}
### integration_test/bots/settings/currency_view_bot.dart
@@ -1,26 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/settings_view/settings_subviews/currency_view.dart';
-import 'package:stackwallet/utilities/cfcolors.dart';
-
-class CurrencyViewBot {
- final WidgetTester tester;
-
- const CurrencyViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(CurrencyView));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byKey(Key("settingsAppBarBackButton")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCurrency(String code) async {
- await tester.tap(find.text(code));
- await tester.pumpAndSettle();
- final text = find.text(code).evaluate().single.widget as Text;
- expect(text.style!.color, CFColors.spark);
- }
-}
### integration_test/bots/settings/network_settings/add_custom_node_view_bot.dart
@@ -1,58 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
-import 'package:stackwallet/widgets/custom_buttons/gradient_button.dart';
-import 'package:stackwallet/widgets/custom_buttons/simple_button.dart';
-
-class AddCustomNodeViewBot {
- final WidgetTester tester;
-
- const AddCustomNodeViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(AddCustomNodeView));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byType(AppBarIconButton));
- await tester.pumpAndSettle();
- }
-
- Future<void> enterNodeName(String name) async {
- await tester.enterText(
- find.byKey(Key("addCustomNodeNodeNameFieldKey")), name);
- await tester.pumpAndSettle();
- }
-
- Future<void> enterNodeAddress(String address) async {
- await tester.enterText(
- find.byKey(Key("addCustomNodeNodeAddressFieldKey")), address);
- await tester.pumpAndSettle();
- }
-
- Future<void> enterNodePort(String port) async {
- await tester.enterText(
- find.byKey(Key("addCustomNodeNodePortFieldKey")), port);
- await tester.pumpAndSettle();
- }
-
- Future<void> tapSSLCheckbox() async {
- await tester.tap(find.byType(Checkbox));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapTestConnection() async {
- await tester.tap(find.byType(SimpleButton));
- await tester.pump(Duration(milliseconds: 500));
- }
-
- Future<void> tapSave() async {
- await tester.fling(
- find.byType(SingleChildScrollView), Offset(0, -500), 10000);
- await tester.pumpAndSettle();
- await tester.tap(find.byType(GradientButton));
- await tester.pumpAndSettle();
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/settings/network_settings/network_settings_view_bot.dart
@@ -1,28 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/settings_view/settings_subviews/network_settings_view.dart';
-
-class NetworkSettingsViewBot {
- final WidgetTester tester;
-
- const NetworkSettingsViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(NetworkSettingsView));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byKey(Key("settingsAppBarBackButton")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapAdd() async {
- await tester.tap(find.byKey(Key("networkSettingsAddNodeButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapNode(String name) async {
- await tester.tap(find.byKey(Key("networkSettingsViewNodeCard_$name")));
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/settings/network_settings/node_details_view_bot.dart
@@ -1,79 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/settings_view/settings_subviews/network_settings_subviews/node_details_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/gradient_button.dart';
-import 'package:stackwallet/widgets/custom_buttons/simple_button.dart';
-
-class NodeDetailsViewBot {
- final WidgetTester tester;
-
- const NodeDetailsViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(NodeDetailsView));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byKey(Key("nodeDetailsViewBackButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapMore() async {
- await tester.tap(find.byKey(Key("nodeDetailsViewMoreButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapMoreAndEdit() async {
- await tapMore();
- await tester.tap(find.text("Edit node"));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapMoreAndDelete() async {
- await tapMore();
- await tester.tap(find.text("Delete node"));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCancelDelete() async {
- await tester
- .tap(find.byKey(Key("nodeDetailsConfirmDeleteCancelButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapConfirmDelete() async {
- await tester
- .tap(find.byKey(Key("nodeDetailsConfirmDeleteConfirmButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> enterName(String name) async {
- await tester.enterText(find.byKey(Key("editNodeNodeNameFieldKey")), name);
- await tester.pump();
- }
-
- Future<void> enterAddress(String address) async {
- await tester.enterText(find.byKey(Key("editNodeAddressFieldKey")), address);
- await tester.pump();
- }
-
- Future<void> enterPort(String port) async {
- await tester.enterText(find.byKey(Key("editNodeNodePortFieldKey")), port);
- await tester.pump();
- }
-
- Future<void> tapUseSSLCheckbox() async {
- await tester.tap(find.byType(Checkbox));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapTestConnection() async {
- await tester.tap(find.byType(SimpleButton));
- await tester.pump(Duration(milliseconds: 500));
- }
-
- Future<void> tapSave() async {
- await tester.tap(find.byType(GradientButton));
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/settings/settings_view_bot.dart
@@ -1,48 +0,0 @@
-import 'package:flutter/cupertino.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/settings_view/settings_view.dart';
-
-class SettingsViewBot {
- final WidgetTester tester;
-
- const SettingsViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(SettingsView));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byKey(Key("settingsAppBarBackButton")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapLogout() async {
- await tester.tap(find.byKey(Key("settingsLogoutAppBarButton")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapAddressBook() async {
- await tester.tap(find.byKey(Key("settingsOptionAddressBook")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapNetwork() async {
- await tester.tap(find.byKey(Key("settingsOptionNetwork")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapWalletBackup() async {
- await tester.tap(find.byKey(Key("settingsOptionWalletBackup")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapWalletSettings() async {
- await tester.tap(find.byKey(Key("settingsOptionWalletSettings")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCurrency() async {
- await tester.tap(find.byKey(Key("settingsOptionCurrency")));
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/settings/wallet_backup_view_bot.dart
@@ -1,34 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/settings_view/settings_subviews/wallet_backup_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
-
-class WalletBackUpViewBot {
- final WidgetTester tester;
-
- const WalletBackUpViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(WalletBackUpView));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byType(AppBarIconButton));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapQrCode() async {
- await tester.tap(find.byKey(Key("walletBackupQrCodeButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapQrCodeCancel() async {
- await tester.tap(find.byKey(Key("walletBackupQrCodeCancelButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCopy() async {
- await tester.tap(find.byKey(Key("walletBackupCopyButtonKey")));
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/settings/wallet_settings/change_pin_view_bot.dart
@@ -1,64 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/settings_view/settings_subviews/wallet_settings_subviews/change_pin_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
-import 'package:stackwallet/widgets/custom_pin_put/pin_keyboard.dart';
-
-class ChangePinViewBot {
- final WidgetTester tester;
-
- const ChangePinViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(ChangePinView));
- }
-
- Future<void> enterPin() async {
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "1"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "2"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "3"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "4"));
- await tester.pumpAndSettle(Duration(seconds: 1));
- }
-
- Future<void> confirmMatchedPin() async {
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "1"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "2"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "3"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "4"));
- await tester.pump(Duration(seconds: 1));
- }
-
- Future<void> confirmUnmatchedPin() async {
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "7"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "9"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "8"));
- await tester.pumpAndSettle(Duration(milliseconds: 500));
- await tester.tap(find.byWidgetPredicate(
- (widget) => widget is NumberKey && widget.number == "5"));
- await tester.pump(Duration(seconds: 1));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byType(AppBarIconButton));
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/settings/wallet_settings/delete_wallet_warning_view_bot.dart
@@ -1,30 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/settings_view/settings_subviews/wallet_settings_subviews/delete_wallet_warning_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/gradient_button.dart';
-import 'package:stackwallet/widgets/custom_buttons/simple_button.dart';
-
-class DeleteWalletWarningViewBot {
- final WidgetTester tester;
-
- const DeleteWalletWarningViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(DeleteWalletWarningView));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byKey(Key("settingsAppBarBackButton")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCancelAndGoBack() async {
- await tester.tap(find.byType(SimpleButton));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapViewBackupKey() async {
- await tester.tap(find.byType(GradientButton));
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/settings/wallet_settings/rename_wallet_view_bot.dart
@@ -1,29 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/settings_view/settings_subviews/wallet_settings_subviews/rename_wallet_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/gradient_button.dart';
-
-class RenameWalletViewBot {
- final WidgetTester tester;
-
- const RenameWalletViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(RenameWalletView));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byKey(Key("settingsAppBarBackButton")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapSave() async {
- await tester.tap(find.byType(GradientButton));
- await tester.pumpAndSettle();
- }
-
- Future<void> enterWalletName(String name) async {
- await tester.enterText(find.byType(TextField), name);
- await tester.pump();
- }
-}
### integration_test/bots/settings/wallet_settings/wallet_delete_mnemonic_view_bot.dart
@@ -1,49 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/gradient_button.dart';
-
-class WalletDeleteMnemonicViewBot {
- final WidgetTester tester;
-
- const WalletDeleteMnemonicViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(WalletDeleteMnemonicView));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byKey(Key("settingsAppBarBackButton")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapQrCode() async {
- await tester.tap(find.byKey(Key("walletDeleteShowQrCodeButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCancelQrCode() async {
- await tester.tap(find.byKey(Key("deleteWalletQrCodePopupCancelButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCopy() async {
- await tester.tap(find.byKey(Key("walletDeleteCopySeedButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapContinue() async {
- await tester.tap(find.byType(GradientButton));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapConfirmContinue() async {
- await tester.tap(find.byKey(Key("walletDeleteContinueDeleteButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCancelContinue() async {
- await tester.tap(find.byKey(Key("walletDeleteContinueCancelButtonKey")));
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/settings/wallet_settings_view_bot.dart
@@ -1,135 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/notifications/campfire_alert.dart';
-import 'package:stackwallet/pages/settings_view/settings_subviews/wallet_settings_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/gradient_button.dart';
-import 'package:stackwallet/widgets/custom_buttons/simple_button.dart';
-
-class WalletSettingsViewBot {
- final WidgetTester tester;
-
- const WalletSettingsViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(WalletSettingsView));
- }
-
- Future<void> tapBack() async {
- await tester.tap(find.byKey(Key("settingsAppBarBackButton")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapChangePIN() async {
- await tester.tap(find.byKey(Key("walletSettingsChangePinButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapToggleBiometrics() async {
- await tester
- .tap(find.byKey(Key("walletSettingsEnableBiometricsButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCancelBiometricsSystemSettingsDialog() async {
- await tester.tap(find.byType(SimpleButton));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapRenameWallet() async {
- await tester.tap(find.byKey(Key("walletSettingsRenameWalletButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapDeleteWallet() async {
- await tester.tap(find.byKey(Key("walletSettingsDeleteWalletButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapFullRescan() async {
- await tester.tap(find.byKey(Key("walletSettingsFullRescanButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCancelDeleteConfirmationDialog() async {
- expect(find.byType(ConfirmationDialog), findsOneWidget);
- expect(find.text("CANCEL"), findsOneWidget);
- expect(find.text("DELETE"), findsOneWidget);
- expect(find.textContaining("Do you want to delete "), findsOneWidget);
- await tester.tap(find.byType(SimpleButton));
- await tester.pumpAndSettle();
- expect(find.byType(ConfirmationDialog), findsNothing);
- }
-
- Future<void> tapDeleteOnDeleteConfirmationDialog() async {
- expect(find.byType(ConfirmationDialog), findsOneWidget);
- expect(find.text("CANCEL"), findsOneWidget);
- expect(find.text("DELETE"), findsOneWidget);
- expect(find.textContaining("Do you want to delete "), findsOneWidget);
- await tester.tap(find.byType(GradientButton));
- await tester.pumpAndSettle();
- expect(find.byType(ConfirmationDialog), findsNothing);
- }
-
- Future<void> tapCancelRescanConfirmationDialog() async {
- expect(find.byType(ConfirmationDialog), findsOneWidget);
- expect(find.text("CANCEL"), findsOneWidget);
- expect(find.text("RESCAN"), findsOneWidget);
- expect(find.text("Are you sure you want to do a full rescan?"),
- findsOneWidget);
- await tester.tap(find.byType(SimpleButton));
- await tester.pumpAndSettle();
- expect(find.byType(ConfirmationDialog), findsNothing);
- }
-
- Future<void> tapRescanOnRescanConfirmationDialog() async {
- expect(find.byType(ConfirmationDialog), findsOneWidget);
- expect(find.text("CANCEL"), findsOneWidget);
- expect(find.text("RESCAN"), findsOneWidget);
- expect(find.text("Are you sure you want to do a full rescan?"),
- findsOneWidget);
- await tester.tap(find.byType(GradientButton));
- await tester.pumpAndSettle();
- expect(find.byType(ConfirmationDialog), findsNothing);
- }
-
- Future<void> tapClearCache() async {
- await tester
- .tap(find.byKey(Key("walletSettingsClearSharedCacheButtonKey")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCancelClearCache() async {
- expect(find.byType(ConfirmationDialog), findsOneWidget);
- expect(find.text("CANCEL"), findsOneWidget);
- expect(find.text("CLEAR"), findsOneWidget);
- expect(
- find.textContaining(
- "Are you sure you want to clear all shared cached "),
- findsOneWidget);
- await tester.tap(find.byType(SimpleButton));
- await tester.pumpAndSettle();
- expect(find.byType(ConfirmationDialog), findsNothing);
- }
-
- Future<void> tapClearOnClearCache() async {
- expect(find.byType(ConfirmationDialog), findsOneWidget);
- expect(find.text("CANCEL"), findsOneWidget);
- expect(find.text("CLEAR"), findsOneWidget);
- expect(
- find.textContaining(
- "Are you sure you want to clear all shared cached "),
- findsOneWidget);
- await tester.tap(find.byType(GradientButton));
- await tester.pumpAndSettle();
- expect(find.byType(ConfirmationDialog), findsNothing);
- }
-
- Future<void> tapOkOnCacheClearedAlert() async {
- expect(find.byType(CampfireAlert), findsOneWidget);
- expect(find.text("Transaction cache cleared!"), findsOneWidget);
- expect(find.text("OK"), findsOneWidget);
- await tester.tap(find.byType(GradientButton));
- await tester.pumpAndSettle();
- expect(find.byType(CampfireAlert), findsNothing);
- }
-}
### integration_test/bots/transaction/transaction_search_view_bot.dart
@@ -1,24 +0,0 @@
-import 'package:flutter/cupertino.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/transaction_subviews/transaction_search_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/simple_button.dart';
-
-class TransactionSearchViewBot {
- final WidgetTester tester;
-
- const TransactionSearchViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(TransactionSearchView));
- }
-
- Future<void> tapX() async {
- await tester.tap(find.byKey(Key("cancelTransactionSearchAppBarButton")));
- await tester.pumpAndSettle();
- }
-
- Future<void> tapCancel() async {
- await tester.tap(find.byType(SimpleButton));
- await tester.pumpAndSettle();
- }
-}
### integration_test/bots/wallet_view_bot.dart
@@ -1,48 +0,0 @@
-import 'package:flutter/cupertino.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
-import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart';
-
-class WalletViewBot {
- final WidgetTester tester;
-
- const WalletViewBot(this.tester);
-
- Future<void> ensureVisible() async {
- await tester.ensureVisible(find.byType(WalletView));
- }
-
- Future<void> tapAvailableFullSwitch() async {
- await tester.tap(find.byType(DraggableSwitchButton));
- await tester.pumpAndSettle();
- }
-
- Future<void> dragAvailableFullSwitchRight() async {
- await tester.fling(
- find.byKey(Key("draggableSwitchButtonSwitch")), Offset(200, 0), 500);
- await tester.pumpAndSettle();
- }
-
- Future<void> dragAvailableFullSwitchLeft() async {
- await tester.fling(
- find.byKey(Key("draggableSwitchButtonSwitch")), Offset(-200, 0), 500);
- await tester.pumpAndSettle();
- }
-
- Future<void> tapTransactionSearch() async {
- await tester.tap(find.byKey(Key("walletViewTransactionSearchButton")));
- await tester.pumpAndSettle();
- }
-
- Future<void> checkAvailableFullSwitchIsEnabled() async {
- final state = tester.state(find.byType(DraggableSwitchButton))
- as DraggableSwitchButtonState;
- expect(state.enabled, true);
- }
-
- Future<void> checkAvailableFullSwitchIsDisabled() async {
- final state = tester.state(find.byType(DraggableSwitchButton))
- as DraggableSwitchButtonState;
- expect(state.enabled, false);
- }
-}
### integration_test/create_new_wallet_test.dart
@@ -1,134 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:integration_test/integration_test.dart';
-import 'package:stackwallet/main.dart' as campfireApp;
-import 'package:stackwallet/pages/main_view.dart';
-
-import 'bot_runners/create_wallet_until_pin_confirmation.dart';
-import 'bots/onboarding/backup_key_view_bot.dart';
-import 'bots/onboarding/backup_key_warning_bot.dart';
-import 'bots/onboarding/create_pin_view_bot.dart';
-import 'bots/onboarding/name_your_wallet_view_bot.dart';
-import 'bots/onboarding/onboarding_view_bot.dart';
-import 'bots/onboarding/terms_and_conditions_bot.dart';
-import 'bots/onboarding/verify_backup_key_view_bot.dart';
-
-void main() {
- group("Campfire app test", () {
- IntegrationTestWidgetsFlutterBinding.ensureInitialized();
- testWidgets("new wallet creation", (tester) async {
- campfireApp.main();
- await tester.pumpAndSettle(Duration(seconds: 10));
-
- final onboardingViewBot = OnboardingViewBot(tester);
- final termsAndConditionsViewBot = TermsAndConditionsViewBot(tester);
- final nameYourWalletViewBot = NameYourWalletViewBot(tester);
- final createPinViewBot = CreatePinViewBot(tester);
- final backupKeyWarningViewBot = BackupKeyWarningViewBot(tester);
- final backupKeyViewBot = BackupKeyViewBot(tester);
- final verifyBackUpViewBot = VerifyBackupKeyViewBot(tester);
-
- // tap create new wallet button
- await onboardingViewBot.ensureVisible();
- await onboardingViewBot.tapCreateNewWallet();
-
- await termsAndConditionsViewBot.ensureVisible();
- // test tap back
- await termsAndConditionsViewBot.tapBack();
- await onboardingViewBot.ensureVisible();
- // tap new again
- await onboardingViewBot.tapCreateNewWallet();
-
- await createWalletUntilPinConfirmation(
- termsAndConditionsViewBot,
- nameYourWalletViewBot,
- createPinViewBot,
- );
-
- // wait for wallet generation
- await tester.pumpAndSettle(Duration(seconds: 60));
-
- await backupKeyWarningViewBot.ensureVisible();
-
- // tap back
- await backupKeyWarningViewBot.tapBack();
- await onboardingViewBot.ensureVisible();
-
- // tap create new wallet button
- await onboardingViewBot.ensureVisible();
- await onboardingViewBot.tapCreateNewWallet();
-
- await termsAndConditionsViewBot.ensureVisible();
- // test tap back
- await termsAndConditionsViewBot.tapBack();
- await onboardingViewBot.ensureVisible();
- // tap new again
- await onboardingViewBot.tapCreateNewWallet();
-
- // run through to backup key warning again
- await createWalletUntilPinConfirmation(
- termsAndConditionsViewBot,
- nameYourWalletViewBot,
- createPinViewBot,
- );
-
- // wait for wallet generation
- await tester.pumpAndSettle(Duration(seconds: 60));
-
- await backupKeyWarningViewBot.ensureVisible();
-
- // enable checkbox
- await backupKeyWarningViewBot.tapCheckBox();
- await backupKeyWarningViewBot.tapViewBackupKey();
-
- // expect to see mnemonic displayed
- await backupKeyViewBot.ensureVisible();
-
- // tap back
- await backupKeyViewBot.tapBack();
- await backupKeyWarningViewBot.ensureVisible();
-
- // advance again
- await backupKeyWarningViewBot.tapViewBackupKey();
- // expect to see mnemonic displayed again
- await backupKeyViewBot.ensureVisible();
-
- // tap qr code
- await backupKeyViewBot.tapQrCode();
- // tap cancel to pop qr code view
- await tester.tap(find.byKey(Key("backUpKeyViewQrCodeCancelButtonKey")));
- await tester.pumpAndSettle();
-
- // tap copy
- await backupKeyViewBot.tapCopy();
-
- // fetch words to verify on next page
- final words = await backupKeyViewBot.displayedMnemonic();
-
- // tap verify to go to next screen
- await backupKeyViewBot.tapVerify();
- // expect next screen
- await verifyBackUpViewBot.ensureVisible();
-
- // ensure overlay from previous copy function has gone away
- await tester.pumpAndSettle(Duration(seconds: 2));
-
- // tap back
- await verifyBackUpViewBot.tapBack();
- await backupKeyViewBot.ensureVisible();
-
- // continue forward again
- await backupKeyViewBot.tapVerify();
- // expect next screen again
- await verifyBackUpViewBot.ensureVisible();
-
- // enter requested word
- await verifyBackUpViewBot.enterRequestedWord(words);
- // tap confirm to check validity
- await verifyBackUpViewBot.tapConfirm();
-
- // expect main wallet view
- expect(find.byType(MainView), findsOneWidget);
- });
- });
-}
### integration_test/desktop_forgot_password/harness.dart
@@ -0,0 +1,302 @@
+// Desktop forgot-password integration tests. With Flutter 3.49 or later:
+//
+// flutter test integration_test/desktop_forgot_password -d macos
+//
+// With older Flutter versions, use the temporary runner:
+//
+// bash scripts/test_desktop_forgot_password.sh macos
+//
+// (or -d linux / -d windows). Linux needs a desktop session, because
+// notifications use D-Bus, and a screen at least 800 px wide, otherwise the
+// app uses its mobile UI.
+//
+// Each test launches the real app against a throwaway folder. App data goes
+// into it, as with the app's -d <dir> flag, and path_provider points into it
+// too, so logs never reach the real documents folder. exit() is intercepted:
+// the test records the exit code and the files on disk when the app quits.
+//
+// One test per file: a reset ends by quitting and the app keeps process-wide
+// state, so every scenario needs its own launch.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:isar_community/isar.dart';
+import 'package:path/path.dart' as p;
+import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
+import 'package:stackwallet/app_config.dart';
+import 'package:stackwallet/db/hive/db.dart';
+import 'package:stackwallet/main.dart' as app;
+import 'package:stackwallet/utilities/desktop_password_service.dart';
+import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
+import 'package:stackwallet/utilities/stack_file_system.dart';
+
+const _password = 'correct horse battery staple';
+const _secretKey = 'test_wallet_mnemonic';
+const _secret = 'test words';
+
+const _walletFiles = [
+ 'wallets/monero/test_wallet/test_wallet',
+ 'wallets/monero/test_wallet/test_wallet.keys',
+ 'epiccash/test_wallet/wallet_data/wallet.seed',
+];
+
+/// Seeded files a reset must delete.
+const seededData = {
+ 'hive/desktopdata.hive', // the password
+ 'isar/desktopStore.isar', // wallet keys
+ ..._walletFiles,
+};
+
+/// Seeded files a reset must keep.
+const seededKeep = {'wallets/backup.swb', 'tor/state'};
+
+/// Plain files the test writes. Each holds its own path.
+const seededFiles = [..._walletFiles, ...seededKeep];
+
+typedef AppExit = ({int code, Set<String> files, List<String> openDatabases});
+
+void desktopTest(String description, Future<void> Function(ResetTest t) body) {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+ testWidgets(
+ description,
+ (tester) async {
+ final temp = await Directory.systemTemp.createTemp('stack_reset_test_');
+ debugPrint('Test folder: ${temp.path}');
+ await body(ResetTest._(tester, temp.path));
+ // Only reached when the test passed, so failed runs keep the folder.
+ // On Windows the still running app holds some of it open.
+ try {
+ await temp.delete(recursive: true);
+ } on FileSystemException catch (_) {}
+ },
+ skip: !(Platform.isLinux || Platform.isMacOS || Platform.isWindows),
+ timeout: const Timeout(Duration(minutes: 3)),
+ );
+}
+
+class ResetTest {
+ ResetTest._(this.tester, this.temp);
+
+ final WidgetTester tester;
+ final String temp;
+ late final String root;
+ late final _io = _TestIO(this);
+ AppExit? _exit;
+ final _errors = <String>[];
+
+ /// Set once the app calls exit().
+ AppExit? get exited => _exit;
+
+ /// Seeds the data folder, then starts the app.
+ Future<void> launch({bool unfinishedReset = false}) async {
+ PathProviderPlatform.instance = _TestPaths(temp);
+ StackFileSystem.setDesktopOverrideDir(p.join(temp, 'data'));
+ root = (await StackFileSystem.applicationRootDirectory()).path;
+
+ DB.instance.hive.init(
+ (await StackFileSystem.applicationHiveDirectory()).path,
+ );
+ final password = DPS();
+ await password.initFromNew(_password);
+ final keys = DesktopSecureStore(password.handler);
+ await keys.init();
+ await keys.write(key: _secretKey, value: _secret);
+ await keys.close();
+
+ for (final file in seededFiles) {
+ await File(path(file)).create(recursive: true);
+ await File(path(file)).writeAsString(file);
+ }
+ // Creating links on Windows needs extra privileges.
+ if (!Platform.isWindows) {
+ await File(p.join(temp, 'outside', 'file')).create(recursive: true);
+ await Link(path('wallets/outside')).create(p.join(temp, 'outside'));
+ }
+ if (unfinishedReset) {
+ await File(path('.reset-pending')).writeAsString('reset');
+ }
+ expect(files(), containsAll({...seededData, ...seededKeep}));
+
+ // Lay out at the largest window main() picks. Smaller windows overflow the
+ // login screen in debug builds, which fails the test.
+ await tester.binding.setSurfaceSize(const Size(1220, 900));
+ // Never restored: the app runs until this test file's process ends.
+ IOOverrides.global = _io;
+ runZonedGuarded(() => app.main(const []), _onAppError);
+ }
+
+ /// Absolute path of a file in the data folder.
+ String path(String file) => p.joinAll([root, ...file.split('/')]);
+
+ /// Everything in the data folder, as paths relative to it.
+ Set<String> files() {
+ final all = Directory(root).listSync(recursive: true, followLinks: false);
+ return {
+ for (final entity in all)
+ p.split(p.relative(entity.path, from: root)).join('/'),
+ };
+ }
+
+ List<String> openDatabases() => [
+ ...Isar.instanceNames,
+ for (final box in [DB.boxNameDBInfo, DB.boxNamePrefs])
+ if (DB.instance.hive.isBoxOpen(box)) box,
+ ];
+
+ /// Makes deleting this file fail, like a file another program has open.
+ void blockDelete(String file) => _io.undeletable = path(file);
+
+ /// Checks seeded files still hold what the test wrote.
+ void expectUnchanged(Iterable<String> files) {
+ for (final file in files) {
+ expect(File(path(file)).readAsStringSync(), file, reason: file);
+ }
+ }
+
+ /// Checks the password still unlocks the seeded wallet key.
+ Future<void> expectPasswordKept() async {
+ final password = DPS();
+ await password.initFromExisting(_password);
+ final keys = DesktopSecureStore(password.handler);
+ await keys.init();
+ expect(await keys.read(key: _secretKey), _secret);
+ await keys.close();
+ }
+
+ void expectLinkTargetKept() {
+ if (Platform.isWindows) return;
+ expect(File(p.join(temp, 'outside', 'file')).existsSync(), isTrue);
+ }
+
+ void expectLogsInTestFolder() => expect(
+ Directory(p.join(temp, 'documents', '${AppConfig.prefix}_Logs'))
+ .existsSync(),
+ isTrue,
+ );
+
+ Future<void> tapText(String text) => tap(find.text(text, findRichText: true));
+
+ Future<void> tap(Finder finder) async {
+ // Exactly one, so a route that is still animating out is not tapped.
+ await _waitUntil(
+ () => finder.hitTestable().evaluate().length == 1,
+ 'Timed out waiting for one $finder',
+ );
+ // Buttons can end in exit(), which throws _AppExited from the callback.
+ await runZonedGuarded(() async {
+ try {
+ await tester.tap(finder.hitTestable());
+ } catch (error, stack) {
+ _onAppError(error, stack);
+ }
+ }, _onAppError);
+ await _pump();
+ }
+
+ Future<void> waitFor(Finder finder) => _waitUntil(
+ () => finder.hitTestable().evaluate().isNotEmpty,
+ 'Timed out waiting for $finder',
+ );
+
+ Future<AppExit> waitForExit() async {
+ await _waitUntil(() => _exit != null, 'The app did not quit');
+ return _exit!;
+ }
+
+ Future<void> _waitUntil(bool Function() done, String reason) async {
+ final deadline = DateTime.now().add(const Duration(minutes: 1));
+ while (true) {
+ await _pump();
+ if (done()) return;
+ if (DateTime.now().isAfter(deadline)) fail(reason);
+ }
+ }
+
+ Future<void> _pump() async {
+ await tester.pump(const Duration(milliseconds: 100));
+ final Object? error = tester.takeException();
+ if (error != null) _onAppError(error, StackTrace.current);
+ if (_errors.isNotEmpty) {
+ fail('The app reported an error:\n${_errors.first}');
+ }
+ }
+
+ void _onAppError(Object error, StackTrace stack) {
+ if (error is! _AppExited) _errors.add('$error\n$stack');
+ }
+}
+
+class _AppExited {
+ const _AppExited();
+}
+
+final class _TestIO extends IOOverrides {
+ _TestIO(this.test);
+
+ final ResetTest test;
+ String? undeletable;
+
+ @override
+ Never exit(int code) {
+ test._exit ??= (
+ code: code,
+ files: test.files(),
+ openDatabases: test.openDatabases(),
+ );
+ throw const _AppExited();
+ }
+
+ // Works because the reset deletes each file through a new File(path).
+ @override
+ File createFile(String path) {
+ final file = super.createFile(path);
+ final blocked = undeletable;
+ return blocked != null && p.equals(path, blocked)
+ ? _UndeletableFile(file)
+ : file;
+ }
+}
+
+class _UndeletableFile extends Fake implements File {
+ _UndeletableFile(this._file);
+
+ final File _file;
+
+ @override
+ String get path => _file.path;
+
+ @override
+ Future<File> delete({bool recursive = false}) async =>
+ throw FileSystemException('Blocked by the test', path);
+}
+
+class _TestPaths extends PathProviderPlatform {
+ _TestPaths(this.temp);
+
+ final String temp;
+
+ Future<String> _directory(String name) async =>
+ (await Directory(p.join(temp, name)).create(recursive: true)).path;
+
+ @override
+ Future<String?> getTemporaryPath() => _directory('temp');
+
+ @override
+ Future<String?> getApplicationSupportPath() => _directory('support');
+
+ @override
+ Future<String?> getLibraryPath() => _directory('library');
+
+ @override
+ Future<String?> getApplicationDocumentsPath() => _directory('documents');
+
+ @override
+ Future<String?> getApplicationCachePath() => _directory('cache');
+
+ @override
+ Future<String?> getDownloadsPath() => _directory('downloads');
+}
### integration_test/desktop_forgot_password/reset_cancelled_test.dart
@@ -0,0 +1,25 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:stackwallet/app_config.dart';
+import 'package:stackwallet/pages_desktop_specific/password/desktop_login_view.dart';
+import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
+
+import 'harness.dart';
+
+void main() {
+ desktopTest('backing out of forgot password deletes nothing', (t) async {
+ await t.launch();
+ await t.waitFor(find.byType(DesktopLoginView));
+ final before = t.files();
+
+ await t.tapText('Forgot password?');
+ await t.tapText('Create new ${AppConfig.prefix}');
+ await t.tapText('Take me back!');
+ await t.tap(find.byType(AppBarBackButton));
+ await t.waitFor(find.byType(DesktopLoginView));
+
+ expect(t.exited, isNull);
+ expect(t.files(), before);
+ t.expectUnchanged(seededFiles);
+ await t.expectPasswordKept();
+ });
+}
### integration_test/desktop_forgot_password/reset_fails_test.dart
@@ -0,0 +1,30 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:stackwallet/app_config.dart';
+
+import 'harness.dart';
+
+void main() {
+ desktopTest('a failed reset quits and stays pending', (t) async {
+ await t.launch();
+ t.blockDelete('wallets/monero/test_wallet/test_wallet.keys');
+ await t.tapText('Forgot password?');
+ await t.tapText('Restore from ${AppConfig.prefix} backup');
+ await t.tapText('Delete everything and quit');
+
+ final exit = await t.waitForExit();
+ expect(exit.code, 1);
+ expect(exit.openDatabases, isEmpty);
+ expect(
+ exit.files,
+ containsAll([
+ '.reset-pending',
+ 'wallets/monero/test_wallet/test_wallet.keys',
+ ]),
+ );
+ // The password and wallet keys go first, so the reset cannot be undone.
+ expect(exit.files, isNot(contains('hive/desktopdata.hive')));
+ expect(exit.files, isNot(contains('isar/desktopStore.isar')));
+ expect(find.textContaining('Reset could not finish'), findsOneWidget);
+ t.expectUnchanged(seededKeep);
+ });
+}
### integration_test/desktop_forgot_password/reset_succeeds_test.dart
@@ -0,0 +1,29 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:stackwallet/app_config.dart';
+
+import 'harness.dart';
+
+void main() {
+ desktopTest('forgot password deletes all app data and quits', (t) async {
+ await t.launch();
+ await t.tapText('Forgot password?');
+ expect(t.openDatabases(), isNotEmpty);
+ await t.tapText('Create new ${AppConfig.prefix}');
+ await t.tapText('Delete everything and quit');
+
+ final exit = await t.waitForExit();
+ expect(exit.code, 0);
+ expect(exit.openDatabases, isEmpty);
+ expect(exit.files, {
+ '.instance.lock',
+ 'tor',
+ 'tor/state',
+ 'wallets',
+ 'wallets/backup.swb',
+ });
+ expect(find.textContaining('was reset and will now close'), findsOneWidget);
+ t.expectUnchanged(seededKeep);
+ t.expectLinkTargetKept();
+ t.expectLogsInTestFolder();
+ });
+}
### integration_test/desktop_forgot_password/unfinished_reset_quit_test.dart
@@ -0,0 +1,20 @@
+import 'package:flutter_test/flutter_test.dart';
+
+import 'harness.dart';
+
+void main() {
+ desktopTest('quitting an unfinished reset keeps everything', (t) async {
+ await t.launch(unfinishedReset: true);
+ await t.waitFor(find.text('Unfinished reset'));
+ final before = t.files();
+ expect(before, contains('.reset-pending'));
+
+ await t.tapText('Quit');
+
+ final exit = await t.waitForExit();
+ expect(exit.code, 0);
+ expect(exit.files, before);
+ t.expectUnchanged(seededFiles);
+ await t.expectPasswordKept();
+ });
+}
### integration_test/desktop_forgot_password/unfinished_reset_retry_test.dart
@@ -0,0 +1,23 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:stackwallet/pages/intro_view.dart';
+import 'package:stackwallet/utilities/desktop_password_service.dart';
+
+import 'harness.dart';
+
+void main() {
+ desktopTest('retrying an unfinished reset deletes the data', (t) async {
+ await t.launch(unfinishedReset: true);
+ await t.tapText('Retry deleting');
+ await t.waitFor(find.byType(IntroView));
+
+ expect(t.exited, isNull);
+ final files = t.files();
+ expect(files, isNot(contains('.reset-pending')));
+ t.expectUnchanged(seededKeep);
+ // The first-run screen opens a new, empty password box.
+ expect(files.intersection(seededData), {'hive/desktopdata.hive'});
+ expect(await DPS().hasPassword(), isFalse);
+ // Logging starts after the reset deleted the settings.
+ t.expectLogsInTestFolder();
+ });
+}
### integration_test/main_wallet_views_test.dart
@@ -1,106 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'package:integration_test/integration_test.dart';
-import 'package:stackwallet/main.dart' as campfireApp;
-
-import 'bot_runners/create_wallet_until_pin_confirmation.dart';
-import 'bots/main_view_bot.dart';
-import 'bots/onboarding/backup_key_warning_bot.dart';
-import 'bots/onboarding/create_pin_view_bot.dart';
-import 'bots/onboarding/name_your_wallet_view_bot.dart';
-import 'bots/onboarding/onboarding_view_bot.dart';
-import 'bots/onboarding/terms_and_conditions_bot.dart';
-import 'bots/receive_view_bot.dart';
-import 'bots/send_view_bot.dart';
-import 'bots/settings/settings_view_bot.dart';
-import 'bots/transaction/transaction_search_view_bot.dart';
-import 'bots/wallet_view_bot.dart';
-
-void main() {
- IntegrationTestWidgetsFlutterBinding.ensureInitialized();
- testWidgets("wallet, send, and receive view test", (tester) async {
- campfireApp.main();
- await tester.pumpAndSettle(Duration(seconds: 10));
-
- // robots
- final onboardingViewBot = OnboardingViewBot(tester);
- final termsAndConditionsViewBot = TermsAndConditionsViewBot(tester);
- final nameYourWalletViewBot = NameYourWalletViewBot(tester);
- final createPinViewBot = CreatePinViewBot(tester);
- final backupKeyWarningViewBot = BackupKeyWarningViewBot(tester);
- final mainViewBot = MainViewBot(tester);
- final settingsViewBot = SettingsViewBot(tester);
- final walletViewBot = WalletViewBot(tester);
- final transactionSearchViewBot = TransactionSearchViewBot(tester);
- final sendViewBot = SendViewBot(tester);
- final receiveViewBot = ReceiveViewBot(tester);
-
- // tap create new wallet button
- await onboardingViewBot.ensureVisible();
- await onboardingViewBot.tapCreateNewWallet();
- await termsAndConditionsViewBot.ensureVisible();
-
- await createWalletUntilPinConfirmation(
- termsAndConditionsViewBot,
- nameYourWalletViewBot,
- createPinViewBot,
- );
-
- // wait for wallet generation
- await tester.pumpAndSettle(Duration(seconds: 60));
-
- await backupKeyWarningViewBot.ensureVisible();
-
- // tap skip to load into main wallet view
- await backupKeyWarningViewBot.tapSkip();
-
- await mainViewBot.ensureVisible();
-
- // tap refresh
- await mainViewBot.tapRefresh();
-
- // tap settings button
- await mainViewBot.tapSettings();
- await settingsViewBot.ensureVisible();
-
- // tap back to main wallet view
- await settingsViewBot.tapBack();
- await mainViewBot.ensureVisible();
-
- await walletViewBot.ensureVisible();
-
- // wait for refresh notification that covers switch to disappear
- await tester.pumpAndSettle(Duration(seconds: 3));
-
- // tap switch
- await walletViewBot.tapAvailableFullSwitch();
- await walletViewBot.checkAvailableFullSwitchIsDisabled();
-
- // drag switch
- await walletViewBot.dragAvailableFullSwitchRight();
- await walletViewBot.checkAvailableFullSwitchIsDisabled();
-
- // drag switch
- await walletViewBot.dragAvailableFullSwitchLeft();
- await walletViewBot.checkAvailableFullSwitchIsEnabled();
-
- // tap switch again
- await walletViewBot.tapAvailableFullSwitch();
- await walletViewBot.checkAvailableFullSwitchIsDisabled();
-
- // tap tx search
- await walletViewBot.tapTransactionSearch();
- await transactionSearchViewBot.ensureVisible();
-
- // go back
- await transactionSearchViewBot.tapX();
- await walletViewBot.ensureVisible();
-
- // go to send tab
- await mainViewBot.tapSend();
- await sendViewBot.ensureVisible();
-
- // go to receive tab
- await mainViewBot.tapReceive();
- await receiveViewBot.ensureVisible();
- });
-}
### integration_test/rescan_test.dart
@@ -1,136 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:integration_test/integration_test.dart';
-import 'package:stackwallet/main.dart' as campfireApp;
-
-import 'bot_runners/create_wallet_until_pin_confirmation.dart';
-import 'bots/lockscreen_view_bot.dart';
-import 'bots/main_view_bot.dart';
-import 'bots/onboarding/create_pin_view_bot.dart';
-import 'bots/onboarding/name_your_wallet_view_bot.dart';
-import 'bots/onboarding/onboarding_view_bot.dart';
-import 'bots/onboarding/restore_wallet_form_view_bot.dart';
-import 'bots/onboarding/terms_and_conditions_bot.dart';
-import 'bots/rescan_warning_view_bot.dart';
-import 'bots/settings/settings_view_bot.dart';
-import 'bots/settings/wallet_settings_view_bot.dart';
-import 'private.dart';
-
-void main() {
- IntegrationTestWidgetsFlutterBinding.ensureInitialized();
- testWidgets("rescan test", (tester) async {
- campfireApp.main();
- await tester.pumpAndSettle(Duration(seconds: 10));
-
- // robots
- final onboardingViewBot = OnboardingViewBot(tester);
- final termsAndConditionsViewBot = TermsAndConditionsViewBot(tester);
- final nameYourWalletViewBot = NameYourWalletViewBot(tester);
- final createPinViewBot = CreatePinViewBot(tester);
-
- // tap restore wallet button
- await onboardingViewBot.ensureVisible();
- await onboardingViewBot.tapRestoreWallet();
- await termsAndConditionsViewBot.ensureVisible();
-
- await createWalletUntilPinConfirmation(
- termsAndConditionsViewBot,
- nameYourWalletViewBot,
- createPinViewBot,
- );
-
- await tester.pumpAndSettle(Duration(seconds: 2));
- final restoreWalletFormViewBot = RestoreWalletFormViewBot(tester);
- await restoreWalletFormViewBot.ensureVisible();
-
- // paste valid mnemonic
- await Clipboard.setData(ClipboardData(text: TEST_MNEMONIC));
- await restoreWalletFormViewBot.tapPaste();
- await restoreWalletFormViewBot.scrollDown();
- await restoreWalletFormViewBot.tapRestore(true);
-
- final mainViewBot = MainViewBot(tester);
- await mainViewBot.ensureVisible();
-
- await Future.delayed(Duration(seconds: 10));
-
- expect(find.byType(ListView, skipOffstage: false), findsNWidgets(1));
- expect(find.text("0.00041252 FIRO"), findsOneWidget);
-
- // restore should have succeeded by now
- // now we test full rescan
- await mainViewBot.tapSettings();
- final settingsViewBot = SettingsViewBot(tester);
- await settingsViewBot.ensureVisible();
- await settingsViewBot.tapWalletSettings();
- final walletSettingsViewBot = WalletSettingsViewBot(tester);
- await walletSettingsViewBot.ensureVisible();
-
- // tap rescan wallet and then cancel
- await walletSettingsViewBot.tapFullRescan();
- await walletSettingsViewBot.tapCancelRescanConfirmationDialog();
-
- // tap rescan and continue running though
- // and testing back taps throughout
- await walletSettingsViewBot.tapFullRescan();
- await walletSettingsViewBot.tapRescanOnRescanConfirmationDialog();
- final lockScreenViewBot = LockscreenViewBot(tester);
- await lockScreenViewBot.ensureVisible();
- await lockScreenViewBot.tapBack();
-
- await walletSettingsViewBot.tapFullRescan();
- await walletSettingsViewBot.tapRescanOnRescanConfirmationDialog();
- await lockScreenViewBot.ensureVisible();
- await lockScreenViewBot.enterPin("9999");
- await tester.pumpAndSettle(Duration(seconds: 2));
- await lockScreenViewBot.ensureVisible();
- await lockScreenViewBot.tapBack();
-
- await walletSettingsViewBot.tapFullRescan();
- await walletSettingsViewBot.tapRescanOnRescanConfirmationDialog();
- await lockScreenViewBot.ensureVisible();
- await lockScreenViewBot.enterPin("1234");
- await tester.pumpAndSettle(Duration(seconds: 2));
- final rescanWarningViewBot = RescanWarningViewBot(tester);
- await rescanWarningViewBot.ensureVisible();
- await rescanWarningViewBot.tapBack();
- await walletSettingsViewBot.ensureVisible();
-
- // tap qr code
- await walletSettingsViewBot.tapFullRescan();
- await walletSettingsViewBot.tapRescanOnRescanConfirmationDialog();
- await lockScreenViewBot.ensureVisible();
- await lockScreenViewBot.enterPin("1234");
- await tester.pumpAndSettle(Duration(seconds: 2));
- await rescanWarningViewBot.ensureVisible();
- await rescanWarningViewBot.tapQrCode();
- await rescanWarningViewBot.tapCancelQrCode();
- await rescanWarningViewBot.ensureVisible();
-
- // tap copy
- await Clipboard.setData(ClipboardData(text: ""));
- await rescanWarningViewBot.tapCopy();
- final mnemonic = (await Clipboard.getData(Clipboard.kTextPlain))!.text;
- expect(mnemonic, TEST_MNEMONIC);
- await tester.pumpAndSettle(Duration(seconds: 2));
- await Clipboard.setData(ClipboardData(text: ""));
- await rescanWarningViewBot.ensureVisible();
-
- // tap continue and cancel
- await rescanWarningViewBot.tapContinue();
- await rescanWarningViewBot.tapCancelContinue();
- await rescanWarningViewBot.ensureVisible();
-
- // tap continue and finally confirm rescan
- await rescanWarningViewBot.tapContinue();
- await rescanWarningViewBot.tapConfirmContinue();
-
- await mainViewBot.ensureVisible();
- await tester.pumpAndSettle();
- await Future.delayed(Duration(seconds: 10));
- await tester.pumpAndSettle();
-
- expect(find.text("0.00041252 FIRO"), findsOneWidget);
- });
-}
### integration_test/restore_wallet_test.dart
@@ -1,124 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:integration_test/integration_test.dart';
-import 'package:stackwallet/main.dart' as campfireApp;
-import 'package:stackwallet/notifications/campfire_alert.dart';
-import 'package:stackwallet/pages/main_view.dart';
-
-import 'bot_runners/create_wallet_until_pin_confirmation.dart';
-import 'bots/onboarding/create_pin_view_bot.dart';
-import 'bots/onboarding/name_your_wallet_view_bot.dart';
-import 'bots/onboarding/onboarding_view_bot.dart';
-import 'bots/onboarding/restore_wallet_form_view_bot.dart';
-import 'bots/onboarding/terms_and_conditions_bot.dart';
-import 'private.dart';
-
-void main() {
- IntegrationTestWidgetsFlutterBinding.ensureInitialized();
- testWidgets("restore from seed test", (tester) async {
- campfireApp.main();
- await tester.pumpAndSettle(Duration(seconds: 10));
-
- // robots
- final onboardingViewBot = OnboardingViewBot(tester);
- final termsAndConditionsViewBot = TermsAndConditionsViewBot(tester);
- final nameYourWalletViewBot = NameYourWalletViewBot(tester);
- final createPinViewBot = CreatePinViewBot(tester);
-
- // tap restore wallet button
- await onboardingViewBot.ensureVisible();
- await onboardingViewBot.tapRestoreWallet();
- await termsAndConditionsViewBot.ensureVisible();
-
- await createWalletUntilPinConfirmation(
- termsAndConditionsViewBot,
- nameYourWalletViewBot,
- createPinViewBot,
- );
-
- await tester.pumpAndSettle(Duration(seconds: 2));
- final restoreWalletFormViewBot = RestoreWalletFormViewBot(tester);
- await restoreWalletFormViewBot.ensureVisible();
-
- // test back
- await restoreWalletFormViewBot.tapBack();
- await onboardingViewBot.ensureVisible();
- await onboardingViewBot.tapRestoreWallet();
- await termsAndConditionsViewBot.ensureVisible();
- await createWalletUntilPinConfirmation(
- termsAndConditionsViewBot,
- nameYourWalletViewBot,
- createPinViewBot,
- );
- await tester.pumpAndSettle();
- await restoreWalletFormViewBot.ensureVisible();
-
- // open qr scanner and cancel
- // this test fails due to system popup?
- // await restoreWalletFormViewBot.tapScanQrCode();
- // await tester.tap(find.text("ONLY THIS TIME"));
- // await tester.pumpAndSettle();
- // await restoreWalletFormViewBot.tapCancelScanQrCode();
- // await restoreWalletFormViewBot.ensureVisible();
-
- // paste invalid mnemonic test
- await Clipboard.setData(ClipboardData(
- text:
- "some mnemonic words some mnemonic words some mnemonic words some mnemonic words some mnemonic words some mnemonic words some mnemonic words some mnemonic words"));
- await restoreWalletFormViewBot.tapPaste();
- await restoreWalletFormViewBot.scrollDown();
- expect(find.text("Please check spelling", skipOffstage: false),
- findsNWidgets(24));
- expect(find.text("some", skipOffstage: false), findsNWidgets(8));
- expect(find.text("mnemonic", skipOffstage: false), findsNWidgets(8));
- expect(find.text("words", skipOffstage: false), findsNWidgets(8));
-
- // tap restore on invalid mnemonic words
- await restoreWalletFormViewBot.tapRestore(true);
- await restoreWalletFormViewBot.ensureVisible();
-
- // paste valid mnemonic
- await Clipboard.setData(ClipboardData(text: TEST_MNEMONIC));
- await restoreWalletFormViewBot.tapPaste();
- await restoreWalletFormViewBot.scrollDown();
-
- // enter a valid word which gives us an invalid mnemonic
- await restoreWalletFormViewBot.enterWord("old", 24);
- await restoreWalletFormViewBot.tapRestore(true);
- expect(find.byType(CampfireAlert), findsOneWidget);
- await tester.tap(find.byKey(Key("campfireAlertOKButtonKey")));
- await tester.pumpAndSettle();
- expect(find.byType(CampfireAlert), findsNothing);
-
- // re paste valid mnemonic, restore, and cancel
- await Clipboard.setData(ClipboardData(text: TEST_MNEMONIC));
- await restoreWalletFormViewBot.tapPaste();
- await restoreWalletFormViewBot.scrollDown();
- await restoreWalletFormViewBot.tapRestore(false);
- await restoreWalletFormViewBot.tapCancelRestore();
- await onboardingViewBot.ensureVisible();
-
- // full restore
- await onboardingViewBot.tapRestoreWallet();
- await termsAndConditionsViewBot.ensureVisible();
- await createWalletUntilPinConfirmation(
- termsAndConditionsViewBot,
- nameYourWalletViewBot,
- createPinViewBot,
- );
- await tester.pumpAndSettle();
- await restoreWalletFormViewBot.ensureVisible();
- await Clipboard.setData(ClipboardData(text: TEST_MNEMONIC));
- await restoreWalletFormViewBot.tapPaste();
- await restoreWalletFormViewBot.scrollDown();
- await restoreWalletFormViewBot.tapRestore(true);
-
- expect(find.byType(MainView), findsOneWidget);
-
- await Future.delayed(Duration(seconds: 10));
-
- expect(find.byType(ListView, skipOffstage: false), findsNWidgets(1));
- expect(find.text("0.00041252 FIRO"), findsOneWidget);
- });
-}
### integration_test/settings_test.dart
@@ -1,643 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:integration_test/integration_test.dart';
-import 'package:stackwallet/main.dart' as campfireApp;
-import 'package:stackwallet/notifications/modal_popup_dialog.dart';
-import 'package:stackwallet/pages/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view.dart';
-import 'package:stackwallet/utilities/address_utils.dart';
-import 'package:stackwallet/widgets/custom_buttons/gradient_button.dart';
-import 'package:stackwallet/widgets/custom_buttons/simple_button.dart';
-import 'package:stackwallet/widgets/node_card.dart';
-import 'package:pretty_qr_code/pretty_qr_code.dart';
-
-import 'bot_runners/create_wallet_until_pin_confirmation.dart';
-import 'bots/addressbook/add_address_book_entry_view_bot.dart';
-import 'bots/addressbook/address_book_card_bot.dart';
-import 'bots/addressbook/address_book_entry_details_view_bot.dart';
-import 'bots/addressbook/address_book_view_bot.dart';
-import 'bots/addressbook/edit_address_book_entry_view_bot.dart';
-import 'bots/lockscreen_view_bot.dart';
-import 'bots/main_view_bot.dart';
-import 'bots/onboarding/backup_key_warning_bot.dart';
-import 'bots/onboarding/create_pin_view_bot.dart';
-import 'bots/onboarding/name_your_wallet_view_bot.dart';
-import 'bots/onboarding/onboarding_view_bot.dart';
-import 'bots/onboarding/terms_and_conditions_bot.dart';
-import 'bots/settings/currency_view_bot.dart';
-import 'bots/settings/network_settings/add_custom_node_view_bot.dart';
-import 'bots/settings/network_settings/network_settings_view_bot.dart';
-import 'bots/settings/network_settings/node_details_view_bot.dart';
-import 'bots/settings/settings_view_bot.dart';
-import 'bots/settings/wallet_backup_view_bot.dart';
-import 'bots/settings/wallet_settings/change_pin_view_bot.dart';
-import 'bots/settings/wallet_settings/delete_wallet_warning_view_bot.dart';
-import 'bots/settings/wallet_settings/rename_wallet_view_bot.dart';
-import 'bots/settings/wallet_settings/wallet_delete_mnemonic_view_bot.dart';
-import 'bots/settings/wallet_settings_view_bot.dart';
-
-const bool TEST_ADDRESS_BOOK = true;
-const bool TEST_NETWORK_SETTINGS = true;
-const bool TEST_WALLET_BACKUP = true;
-const bool TEST_CURRENCY = true;
-const bool TEST_WALLET_SETTINGS = true;
-
-void main() {
- IntegrationTestWidgetsFlutterBinding.ensureInitialized();
- // may fail if a network connection fails somewhere
- testWidgets("settings integration test", (tester) async {
- campfireApp.main();
- await tester.pumpAndSettle(Duration(seconds: 10));
-
- // robots
- final onboardingViewBot = OnboardingViewBot(tester);
- final termsAndConditionsViewBot = TermsAndConditionsViewBot(tester);
- final nameYourWalletViewBot = NameYourWalletViewBot(tester);
- final createPinViewBot = CreatePinViewBot(tester);
- final backupKeyWarningViewBot = BackupKeyWarningViewBot(tester);
- final mainViewBot = MainViewBot(tester);
- final settingsViewBot = SettingsViewBot(tester);
-
- // tap create new wallet button
- await onboardingViewBot.ensureVisible();
- await onboardingViewBot.tapCreateNewWallet();
- await termsAndConditionsViewBot.ensureVisible();
-
- await createWalletUntilPinConfirmation(
- termsAndConditionsViewBot,
- nameYourWalletViewBot,
- createPinViewBot,
- );
-
- // wait for wallet generation
- await tester.pumpAndSettle(Duration(seconds: 60));
-
- await backupKeyWarningViewBot.ensureVisible();
-
- // tap skip to load into main wallet view
- await backupKeyWarningViewBot.tapSkip();
- await mainViewBot.ensureVisible();
-
- // tap settings button
- await mainViewBot.tapSettings();
- await settingsViewBot.ensureVisible();
-
- if (TEST_ADDRESS_BOOK) {
- // tap address book
- await settingsViewBot.tapAddressBook();
- final addressBookViewBot = AddressBookViewBot(tester);
- await addressBookViewBot.ensureVisible();
-
- // add entry
- await addressBookViewBot.tapAdd();
- final addAddressBookEntryViewBot = AddAddressBookEntryViewBot(tester);
- await addAddressBookEntryViewBot.ensureVisible();
-
- // test back and cancel
- await addAddressBookEntryViewBot.tapBack();
- await addressBookViewBot.ensureVisible();
- await addressBookViewBot.tapAdd();
- await addAddressBookEntryViewBot.tapCancel();
- await addressBookViewBot.ensureVisible();
-
- // now add an entry
- await addressBookViewBot.tapAdd();
- await addAddressBookEntryViewBot.ensureVisible();
- await addAddressBookEntryViewBot
- .enterAddress("aPjLWDTPQsoPHUTxKBNRzoebDALj3eTcfh");
- await addAddressBookEntryViewBot.enterName("john doe");
-
- // save entry
- await addAddressBookEntryViewBot.tapSave();
- await addressBookViewBot.ensureVisible();
- final addressBookCardBot = AddressBookCardBot(tester);
- await addressBookCardBot.ensureVisible();
-
- // expand options
- await addressBookCardBot.toggleExpandCard();
-
- // tap copy
- await addressBookCardBot.tapCopy();
- expect((await Clipboard.getData(Clipboard.kTextPlain))!.text,
- "aPjLWDTPQsoPHUTxKBNRzoebDALj3eTcfh");
- expect(find.text("Address copied to clipboard"), findsOneWidget);
- await tester.pumpAndSettle(Duration(seconds: 2));
-
- // clear clipboard
- await Clipboard.setData(ClipboardData(text: ""));
-
- // tap details
- await addressBookCardBot.tapDetails();
- final addressBookEntryDetailsViewBot =
- AddressBookEntryDetailsViewBot(tester);
- await addressBookEntryDetailsViewBot.ensureVisible();
-
- // tap back then go back to details
- await addressBookEntryDetailsViewBot.tapBack();
- await addressBookCardBot.ensureVisible();
- await addressBookCardBot.tapDetails();
- await addressBookEntryDetailsViewBot.ensureVisible();
-
- // copy address
- await addressBookEntryDetailsViewBot.tapCopyAddress();
- expect((await Clipboard.getData(Clipboard.kTextPlain))!.text,
- "aPjLWDTPQsoPHUTxKBNRzoebDALj3eTcfh");
- expect(find.text("Address copied to clipboard"), findsOneWidget);
- await tester.pumpAndSettle(Duration(seconds: 2));
-
- // clear clipboard
- await Clipboard.setData(ClipboardData(text: ""));
-
- // delete and cancel
- await addressBookEntryDetailsViewBot.tapMore();
- await addressBookEntryDetailsViewBot.tapDelete();
- await addressBookEntryDetailsViewBot.tapCancelDelete();
-
- // delete and confirm
- await addressBookEntryDetailsViewBot.tapMore();
- await addressBookEntryDetailsViewBot.tapDelete();
- await addressBookEntryDetailsViewBot.tapConfirmDelete();
- await addressBookViewBot.ensureVisible();
-
- // add and save another entry, then go to details
- await addressBookViewBot.tapAdd();
- await addAddressBookEntryViewBot.ensureVisible();
- await addAddressBookEntryViewBot
- .enterAddress("aPjLWDTPQsoPHUTxKBNRzoebDALj3eTcfh");
- await addAddressBookEntryViewBot.enterName("john doe");
- await addAddressBookEntryViewBot.tapSave();
- await addressBookViewBot.ensureVisible();
- await addressBookCardBot.ensureVisible();
- await addressBookCardBot.toggleExpandCard();
- await addressBookCardBot.tapDetails();
- await addressBookEntryDetailsViewBot.ensureVisible();
-
- // tap edit
- await addressBookEntryDetailsViewBot.tapEdit();
- final editAddressBookEntryViewBot = EditAddressBookEntryViewBot(tester);
- await editAddressBookEntryViewBot.ensureVisible();
-
- // tap back
- await editAddressBookEntryViewBot.tapBack();
- await addressBookEntryDetailsViewBot.ensureVisible();
- await addressBookEntryDetailsViewBot.tapEdit();
- await editAddressBookEntryViewBot.ensureVisible();
-
- // tap cancel
- await editAddressBookEntryViewBot.tapCancel();
- await addressBookEntryDetailsViewBot.ensureVisible();
- await addressBookEntryDetailsViewBot.tapEdit();
- await editAddressBookEntryViewBot.ensureVisible();
-
- // tap save without editing
- await editAddressBookEntryViewBot.tapSave();
- await addressBookEntryDetailsViewBot.ensureVisible();
- await addressBookEntryDetailsViewBot.tapEdit();
- await editAddressBookEntryViewBot.ensureVisible();
-
- // tap save with editing
- await editAddressBookEntryViewBot.enterName("jane doe");
- await editAddressBookEntryViewBot
- .enterAddress("aPjLWDTPQsoPHUTxKBNRzoebDALj3eTcfh");
-
- await editAddressBookEntryViewBot.tapSave();
- await addressBookViewBot.ensureVisible();
- await addressBookCardBot.ensureVisible();
-
- // expand and tap send
- await addressBookCardBot.toggleExpandCard();
- await addressBookCardBot.tapSend();
- await mainViewBot.ensureVisible();
-
- // go to details and send
- await mainViewBot.tapSettings();
- await settingsViewBot.tapAddressBook();
- await addressBookCardBot.toggleExpandCard();
- await addressBookCardBot.tapDetails();
- await addressBookEntryDetailsViewBot.tapSend();
- await mainViewBot.ensureVisible();
-
- // open settings again to continue testing
- await mainViewBot.tapSettings();
- await settingsViewBot.ensureVisible();
- }
-
- if (TEST_NETWORK_SETTINGS) {
- // network settings
- await settingsViewBot.tapNetwork();
- final networkSettingsViewBot = NetworkSettingsViewBot(tester);
- await networkSettingsViewBot.ensureVisible();
-
- // test back
- await networkSettingsViewBot.tapBack();
- await settingsViewBot.tapNetwork();
- await networkSettingsViewBot.ensureVisible();
- expect(find.byType(NodeCard), findsOneWidget);
-
- // test add node back
- await networkSettingsViewBot.tapAdd();
- final addCustomNodeViewBot = AddCustomNodeViewBot(tester);
- await addCustomNodeViewBot.ensureVisible();
- await addCustomNodeViewBot.tapBack();
- await networkSettingsViewBot.tapAdd();
- await addCustomNodeViewBot.ensureVisible();
-
- // test add node
- await addCustomNodeViewBot.enterNodeName("MyNode");
- await addCustomNodeViewBot.enterNodeAddress("MyNodeAddress");
- await addCustomNodeViewBot.enterNodePort("0");
-
- // tap test connection
- await addCustomNodeViewBot.tapTestConnection();
- expect(find.text("Connection failed!"), findsOneWidget);
- await tester.pumpAndSettle(Duration(seconds: 2));
-
- // test save non connecting node
- await addCustomNodeViewBot.tapSave();
- expect(find.byType(CouldNotConnectOnSaveDialog), findsOneWidget);
- await tester
- .tap(find.byKey(Key("couldNotConnectOnSaveConfirmSaveButtonKey")));
- await tester.pumpAndSettle();
- await networkSettingsViewBot.ensureVisible();
- expect(find.byType(NodeCard), findsNWidgets(2));
-
- // tap default node to get context menu
- expect(find.text("Connect"), findsNothing);
- expect(find.text("Details"), findsNothing);
- expect(find.text("Edit"), findsNothing);
- expect(find.text("Delete"), findsNothing);
- await networkSettingsViewBot.tapNode("Campfire default");
- expect(find.text("Connect"), findsOneWidget);
- expect(find.text("Details"), findsOneWidget);
- expect(find.text("Edit"), findsNothing);
- expect(find.text("Delete"), findsNothing);
-
- // tap connect
- await tester.tap(find.text("Connect"));
- await tester.pumpAndSettle();
-
- // tap details
- await networkSettingsViewBot.tapNode("Campfire default");
- await tester.tap(find.text("Details"));
- await tester.pumpAndSettle();
- final nodeDetailsViewBot = NodeDetailsViewBot(tester);
- await nodeDetailsViewBot.ensureVisible();
-
- // tap test connection
- await nodeDetailsViewBot.tapTestConnection();
- await tester.pumpAndSettle(Duration(seconds: 3));
-
- // tap back
- await nodeDetailsViewBot.tapBack();
- await networkSettingsViewBot.ensureVisible();
-
- // tap custom node to get context menu
- expect(find.text("Connect"), findsNothing);
- expect(find.text("Details"), findsNothing);
- expect(find.text("Edit"), findsNothing);
- expect(find.text("Delete"), findsNothing);
- await networkSettingsViewBot.tapNode("MyNode");
- expect(find.text("Connect"), findsOneWidget);
- expect(find.text("Details"), findsOneWidget);
- expect(find.text("Edit"), findsOneWidget);
- expect(find.text("Delete"), findsOneWidget);
-
- // tap details
- await tester.tap(find.text("Details"));
- await tester.pumpAndSettle();
- await nodeDetailsViewBot.ensureVisible();
-
- // test back
- await nodeDetailsViewBot.tapBack();
- await networkSettingsViewBot.ensureVisible();
- await networkSettingsViewBot.tapNode("MyNode");
- await tester.tap(find.text("Details"));
- await tester.pumpAndSettle();
- await nodeDetailsViewBot.ensureVisible();
-
- // tap test connection
- await nodeDetailsViewBot.tapTestConnection();
- expect(find.text("Connection failed!"), findsOneWidget);
- await tester.pumpAndSettle(Duration(seconds: 2));
-
- // open context and edit
- await nodeDetailsViewBot.tapMoreAndEdit();
-
- // tap disabled save
- await nodeDetailsViewBot.tapSave();
- await nodeDetailsViewBot.ensureVisible();
-
- // edit and save node
- await nodeDetailsViewBot.tapUseSSLCheckbox();
- await nodeDetailsViewBot.enterName("MyNode2");
- await nodeDetailsViewBot.tapSave();
- await networkSettingsViewBot.ensureVisible();
- expect(find.text("MyNode2"), findsOneWidget);
- expect(find.text("MyNode"), findsNothing);
-
- // tap delete node from details view
- await networkSettingsViewBot.tapNode("MyNode2");
- await tester.tap(find.text("Details"));
- await tester.pumpAndSettle();
- await nodeDetailsViewBot.tapMoreAndDelete();
-
- // cancel delete
- expect(find.byType(ModalPopupDialog), findsOneWidget);
- await nodeDetailsViewBot.tapCancelDelete();
- expect(find.byType(ModalPopupDialog), findsNothing);
-
- // confirm delete
- await nodeDetailsViewBot.tapMoreAndDelete();
- await nodeDetailsViewBot.tapConfirmDelete();
- await networkSettingsViewBot.ensureVisible();
- // expect only the default node now
- expect(find.byType(NodeCard), findsOneWidget);
-
- // create new node to test remaining two context options
- // on the main network settings page
- await networkSettingsViewBot.tapAdd();
- await addCustomNodeViewBot.ensureVisible();
- await addCustomNodeViewBot.enterNodeName("MyNode");
- await addCustomNodeViewBot.enterNodeAddress("MyNodeAddress");
- await addCustomNodeViewBot.enterNodePort("0");
- await addCustomNodeViewBot.tapSave();
- await tester
- .tap(find.byKey(Key("couldNotConnectOnSaveConfirmSaveButtonKey")));
- await tester.pumpAndSettle();
- await networkSettingsViewBot.ensureVisible();
- expect(find.byType(NodeCard), findsNWidgets(2));
-
- // tap edit node from the main network settings page, then save
- await networkSettingsViewBot.tapNode("MyNode");
- await tester.tap(find.text("Edit"));
- await tester.pumpAndSettle();
- await nodeDetailsViewBot.ensureVisible();
- await nodeDetailsViewBot.enterName("MyNode3");
- await nodeDetailsViewBot.enterAddress("somenewaddress");
- await nodeDetailsViewBot.enterPort("00");
- await nodeDetailsViewBot.tapUseSSLCheckbox();
- await tester.tap(find.text("Use SSL"));
- await tester.pumpAndSettle();
- await nodeDetailsViewBot.tapSave();
- await networkSettingsViewBot.ensureVisible();
-
- // tap edit node from the main network settings page, then back
- await networkSettingsViewBot.tapNode("MyNode3");
- await tester.tap(find.text("Edit"));
- await tester.pumpAndSettle();
- await nodeDetailsViewBot.tapBack();
- await networkSettingsViewBot.ensureVisible();
-
- // tap delete node from the main network settings page, then cancel
- await networkSettingsViewBot.tapNode("MyNode3");
- await tester.tap(find.text("Delete"));
- await tester.pumpAndSettle();
- await tester.tap(find.byType(SimpleButton));
- await tester.pumpAndSettle();
- await networkSettingsViewBot.ensureVisible();
- expect(find.byType(NodeCard), findsNWidgets(2));
-
- // tap delete node from the main network settings page, then confirm
- await networkSettingsViewBot.tapNode("MyNode3");
- await tester.tap(find.text("Delete"));
- await tester.pumpAndSettle();
- await tester.tap(find.byType(GradientButton));
- await tester.pumpAndSettle();
- await networkSettingsViewBot.ensureVisible();
- expect(find.byType(NodeCard), findsOneWidget);
-
- // go back to main settings
- await networkSettingsViewBot.tapBack();
- await settingsViewBot.ensureVisible();
- }
-
- if (TEST_WALLET_BACKUP) {
- await settingsViewBot.tapWalletBackup();
- final lockscreenViewBot = LockscreenViewBot(tester);
- await lockscreenViewBot.ensureVisible();
-
- // test back
- await lockscreenViewBot.tapBack();
- await settingsViewBot.ensureVisible();
- await settingsViewBot.tapWalletBackup();
-
- // enter wrong pin
- await lockscreenViewBot.enterPin("2222");
- await tester.pumpAndSettle(Duration(seconds: 2));
- await lockscreenViewBot.ensureVisible();
-
- // test back again
- await lockscreenViewBot.tapBack();
- await settingsViewBot.ensureVisible();
- await settingsViewBot.tapWalletBackup();
-
- // enter correct pin
- await lockscreenViewBot.enterPin("1234");
- await tester.pumpAndSettle(Duration(seconds: 2));
- final walletBackUpViewBot = WalletBackUpViewBot(tester);
- await walletBackUpViewBot.ensureVisible();
-
- // tap copy
- await Clipboard.setData(ClipboardData(text: ""));
- await walletBackUpViewBot.tapCopy();
- final mnemonic =
- (await Clipboard.getData(Clipboard.kTextPlain))!.text!.split(" ");
- expect(mnemonic.length, 24);
- await tester.pumpAndSettle(Duration(seconds: 2));
- await Clipboard.setData(ClipboardData(text: ""));
-
- // tap qr code then cancel
- await walletBackUpViewBot.tapQrCode();
- final qr = find.byType(PrettyQr).evaluate().first.widget as PrettyQr;
- expect(qr.data, AddressUtils.encodeQRSeedData(mnemonic));
- await walletBackUpViewBot.tapQrCodeCancel();
- expect(find.byType(ModalPopupDialog), findsNothing);
-
- // tap back to settings
- await walletBackUpViewBot.tapBack();
- await settingsViewBot.ensureVisible();
- }
-
- if (TEST_CURRENCY) {
- // change to a different currency from default USD
- await settingsViewBot.tapCurrency();
- final currencyViewBot = CurrencyViewBot(tester);
- await currencyViewBot.ensureVisible();
- await currencyViewBot.tapCurrency("CHF");
-
- // go back to main view to check for "CHF"
- await currencyViewBot.tapBack();
- await settingsViewBot.ensureVisible();
- await settingsViewBot.tapBack();
- await mainViewBot.ensureVisible();
- expect(find.text("CHF"), findsOneWidget);
-
- // open settings again to continue testing
- await mainViewBot.tapSettings();
- await settingsViewBot.ensureVisible();
- }
-
- if (TEST_WALLET_SETTINGS) {
- // tap wallet settings
- await settingsViewBot.tapWalletSettings();
- final walletSettingsViewBot = WalletSettingsViewBot(tester);
- await walletSettingsViewBot.ensureVisible();
-
- // test all change pin back taps
- await walletSettingsViewBot.tapChangePIN();
- final lockScreenViewBot = LockscreenViewBot(tester);
- await lockScreenViewBot.ensureVisible();
- await lockScreenViewBot.tapBack();
- await walletSettingsViewBot.tapChangePIN();
- await lockScreenViewBot.enterPin("1234");
- await tester.pumpAndSettle(Duration(seconds: 2));
- final changePinViewBot = ChangePinViewBot(tester);
- await changePinViewBot.ensureVisible();
- await changePinViewBot.tapBack();
- await walletSettingsViewBot.tapChangePIN();
- await lockScreenViewBot.enterPin("1234");
- await tester.pumpAndSettle(Duration(seconds: 2));
- await changePinViewBot.enterPin();
- await changePinViewBot.tapBack();
- await walletSettingsViewBot.ensureVisible();
-
- // change pin with a few fails
- await walletSettingsViewBot.tapChangePIN();
- await lockScreenViewBot.enterPin("2222");
- await tester.pumpAndSettle(Duration(seconds: 2));
- await lockScreenViewBot.enterPin("1234");
- await tester.pumpAndSettle(Duration(seconds: 2));
- await changePinViewBot.enterPin();
- await changePinViewBot.confirmUnmatchedPin();
- await changePinViewBot.enterPin();
- await changePinViewBot.confirmUnmatchedPin();
- await changePinViewBot.enterPin();
- await changePinViewBot.confirmMatchedPin();
- await walletSettingsViewBot.ensureVisible();
- await tester.pumpAndSettle(Duration(seconds: 2));
-
- // change pin with no fails
- await walletSettingsViewBot.tapChangePIN();
- await lockScreenViewBot.enterPin("2222");
- await tester.pumpAndSettle(Duration(seconds: 2));
- await lockScreenViewBot.enterPin("1234");
- await tester.pumpAndSettle(Duration(seconds: 2));
- await changePinViewBot.enterPin();
- await changePinViewBot.confirmMatchedPin();
- await walletSettingsViewBot.ensureVisible();
- await tester.pumpAndSettle(Duration(seconds: 2));
-
- // biometrics doesn't test well as it prompts for system settings
- // await walletSettingsViewBot.tapToggleBiometrics();
- // await walletSettingsViewBot.tapCancelBiometricsSystemSettingsDialog();
-
- // rename wallet save
- await walletSettingsViewBot.tapRenameWallet();
- final renameWalletViewBot = RenameWalletViewBot(tester);
- await renameWalletViewBot.tapSave();
- await renameWalletViewBot.enterWalletName("My Firo Wallet2");
- await renameWalletViewBot.tapSave();
-
- await walletSettingsViewBot.tapRenameWallet();
- expect(find.text("My Firo Wallet2"), findsOneWidget);
- await renameWalletViewBot.enterWalletName("My Firo Wallet");
- await renameWalletViewBot.tapSave();
-
- // tap back
- await walletSettingsViewBot.tapRenameWallet();
- await renameWalletViewBot.tapBack();
- await walletSettingsViewBot.ensureVisible();
-
- // tap clear cache and cancel
- await walletSettingsViewBot.tapClearCache();
- await walletSettingsViewBot.tapCancelClearCache();
- await walletSettingsViewBot.ensureVisible();
-
- // tap clear cache and clear
- await walletSettingsViewBot.tapClearCache();
- await walletSettingsViewBot.tapClearOnClearCache();
- await walletSettingsViewBot.tapOkOnCacheClearedAlert();
- await walletSettingsViewBot.ensureVisible();
-
- // tap delete wallet and cancel
- await walletSettingsViewBot.tapDeleteWallet();
- await walletSettingsViewBot.tapCancelDeleteConfirmationDialog();
-
- // tap delete wallet and continue running though
- // and testing back taps throughout
- await walletSettingsViewBot.tapDeleteWallet();
- await walletSettingsViewBot.tapDeleteOnDeleteConfirmationDialog();
- await lockScreenViewBot.ensureVisible();
- await lockScreenViewBot.tapBack();
-
- await walletSettingsViewBot.tapDeleteWallet();
- await walletSettingsViewBot.tapDeleteOnDeleteConfirmationDialog();
- await lockScreenViewBot.ensureVisible();
- await lockScreenViewBot.enterPin("9999");
- await tester.pumpAndSettle(Duration(seconds: 2));
- await lockScreenViewBot.ensureVisible();
- await lockScreenViewBot.tapBack();
-
- await walletSettingsViewBot.tapDeleteWallet();
- await walletSettingsViewBot.tapDeleteOnDeleteConfirmationDialog();
- await lockScreenViewBot.ensureVisible();
- await lockScreenViewBot.enterPin("1234");
- await tester.pumpAndSettle(Duration(seconds: 2));
- final deleteWalletWarningViewBot = DeleteWalletWarningViewBot(tester);
- await deleteWalletWarningViewBot.ensureVisible();
- await deleteWalletWarningViewBot.tapBack();
- await walletSettingsViewBot.ensureVisible();
-
- await walletSettingsViewBot.tapDeleteWallet();
- await walletSettingsViewBot.tapDeleteOnDeleteConfirmationDialog();
- await lockScreenViewBot.ensureVisible();
- await lockScreenViewBot.enterPin("1234");
- await tester.pumpAndSettle(Duration(seconds: 2));
- await deleteWalletWarningViewBot.ensureVisible();
- await deleteWalletWarningViewBot.tapCancelAndGoBack();
- await walletSettingsViewBot.ensureVisible();
-
- // finally go through and tap view backup key
- await walletSettingsViewBot.tapDeleteWallet();
- await walletSettingsViewBot.tapDeleteOnDeleteConfirmationDialog();
- await lockScreenViewBot.ensureVisible();
- await lockScreenViewBot.enterPin("1234");
- await tester.pumpAndSettle(Duration(seconds: 2));
- await deleteWalletWarningViewBot.ensureVisible();
- await deleteWalletWarningViewBot.tapViewBackupKey();
- final walletDeleteMnemonicViewBot = WalletDeleteMnemonicViewBot(tester);
- await walletDeleteMnemonicViewBot.ensureVisible();
-
- // test back
- await walletDeleteMnemonicViewBot.tapBack();
- await deleteWalletWarningViewBot.ensureVisible();
- await deleteWalletWarningViewBot.tapViewBackupKey();
- await walletDeleteMnemonicViewBot.ensureVisible();
-
- // tap qr code
- await walletDeleteMnemonicViewBot.tapQrCode();
- await walletDeleteMnemonicViewBot.tapCancelQrCode();
- await walletDeleteMnemonicViewBot.ensureVisible();
-
- // tap copy
- await Clipboard.setData(ClipboardData(text: ""));
- await walletDeleteMnemonicViewBot.tapCopy();
- final mnemonic =
- (await Clipboard.getData(Clipboard.kTextPlain))!.text!.split(" ");
- expect(mnemonic.length, 24);
- await tester.pumpAndSettle(Duration(seconds: 2));
- await Clipboard.setData(ClipboardData(text: ""));
- await walletDeleteMnemonicViewBot.ensureVisible();
-
- // tap continue and cancel
- await walletDeleteMnemonicViewBot.tapContinue();
- await walletDeleteMnemonicViewBot.tapCancelContinue();
- await walletDeleteMnemonicViewBot.ensureVisible();
-
- // tap continue and finally confirm deletion
- await walletDeleteMnemonicViewBot.tapContinue();
- await walletDeleteMnemonicViewBot.tapConfirmContinue();
- await onboardingViewBot.ensureVisible();
- }
- });
-}
### lib/db/sqlite/firo_cache_coordinator.dart
@@ -23,6 +23,17 @@ abstract class FiroCacheCoordinator {
}
}
+ /// Terminal shutdown for a desktop reset, followed by process exit.
+ static Future<void> close() async {
+ await Future.wait(_workers.values.map((worker) => worker.close()));
+ for (final db in [
+ ..._FiroCache._setCacheDB.values,
+ ..._FiroCache._usedTagsCacheDB.values,
+ ]) {
+ db.close();
+ }
+ }
+
static Future<void> clearSharedCache(
CryptoCurrencyNetwork network, {
bool clearOnlyUsedTagsCache = false,
@@ -42,12 +53,12 @@ abstract class FiroCacheCoordinator {
"${dir.path}/${_FiroCache.sparkUsedTagsCacheFileName(network)}",
);
- final setSize =
- (await setCacheFile.exists()) ? await setCacheFile.length() : 0;
- final tagsSize =
- (await usedTagsCacheFile.exists())
- ? await usedTagsCacheFile.length()
- : 0;
+ final setSize = (await setCacheFile.exists())
+ ? await setCacheFile.length()
+ : 0;
+ final tagsSize = (await usedTagsCacheFile.exists())
+ ? await usedTagsCacheFile.length()
+ : 0;
Logging.instance.d("Spark cache used tags size: $tagsSize");
Logging.instance.d("Spark cache anon set size: $setSize");
@@ -91,8 +102,7 @@ abstract class FiroCacheCoordinator {
void Function(int countFetched, int totalCount)? progressUpdated,
) async {
await _setLocks[network]!.protect(() async {
- const sectorSize =
- 1500; // chosen as a somewhat decent value. Could be changed in the future if wanted/needed
+ const sectorSize = 1500; // chosen as a somewhat decent value. Could be changed in the future if wanted/needed
final prevMeta = await FiroCacheCoordinator.getLatestSetInfoForGroupId(
groupId,
network,
@@ -141,10 +151,9 @@ abstract class FiroCacheCoordinator {
coins.addAll(data);
}
- final result =
- coins
- .map((e) => RawSparkCoin.fromRPCResponse(e as List, groupId))
- .toList();
+ final result = coins
+ .map((e) => RawSparkCoin.fromRPCResponse(e as List, groupId))
+ .toList();
await _workers[network]!.runTask(
FCTask(
@@ -224,17 +233,16 @@ abstract class FiroCacheCoordinator {
String? afterBlockHash,
required CryptoCurrencyNetwork network,
}) async {
- final resultSet =
- afterBlockHash == null
- ? await _Reader._getSetCoinsForGroupId(
- groupId,
- db: _FiroCache.setCacheDB(network),
- )
- : await _Reader._getSetCoinsForGroupIdAndBlockHash(
- groupId,
- afterBlockHash,
- db: _FiroCache.setCacheDB(network),
- );
+ final resultSet = afterBlockHash == null
+ ? await _Reader._getSetCoinsForGroupId(
+ groupId,
+ db: _FiroCache.setCacheDB(network),
+ )
+ : await _Reader._getSetCoinsForGroupIdAndBlockHash(
+ groupId,
+ afterBlockHash,
+ db: _FiroCache.setCacheDB(network),
+ );
return resultSet
.map(
### lib/db/sqlite/firo_cache_worker.dart
@@ -3,6 +3,7 @@ part of 'firo_cache.dart';
enum FCFuncName {
_updateSparkAnonSetCoinsWith,
_updateSparkUsedTagsWith,
+ _close,
}
class FCTask {
@@ -16,9 +17,29 @@ class FCTask {
class _FiroCacheWorker {
final SendPort _commands;
final ReceivePort _responses;
+ final Isolate _isolate;
final Map<String, Completer<Object?>> _activeRequests = {};
+ bool _closing = false;
+
+ Future<void> close() async {
+ final closed = runTask(FCTask(func: FCFuncName._close, data: null));
+ _closing = true;
+ try {
+ await closed.timeout(const Duration(seconds: 15));
+ } catch (_) {
+ // The worker may have exited before receiving the close request, or
+ // may be stuck in a task. The reset caller exits and retries deletion
+ // on the next launch instead of deleting files under a live worker.
+ _isolate.kill(priority: Isolate.immediate);
+ rethrow;
+ } finally {
+ _responses.close();
+ }
+ }
+
Future<Object?> runTask(FCTask task) async {
+ if (_closing) throw StateError('Firo cache worker is closing');
final completer = Completer<Object?>.sync();
_activeRequests[task.id] = completer;
_commands.send(task);
@@ -37,34 +58,30 @@ class _FiroCacheWorker {
initPort.handler = (dynamic initialMessage) {
final commandPort = initialMessage as SendPort;
- connection.complete(
- (
- ReceivePort.fromRawReceivePort(initPort),
- commandPort,
- ),
- );
+ connection.complete((
+ ReceivePort.fromRawReceivePort(initPort),
+ commandPort,
+ ));
};
+ final Isolate isolate;
try {
- await Isolate.spawn(
- _startWorkerIsolate,
- (
- initPort.sendPort,
- setCacheFilePath,
- usedTagsCacheFilePath,
- ),
- );
+ isolate = await Isolate.spawn(_startWorkerIsolate, (
+ initPort.sendPort,
+ setCacheFilePath,
+ usedTagsCacheFilePath,
+ ));
} catch (_) {
initPort.close();
rethrow;
}
final (receivePort, sendPort) = await connection.future;
- return _FiroCacheWorker._(receivePort, sendPort);
+ return _FiroCacheWorker._(receivePort, sendPort, isolate);
}
- _FiroCacheWorker._(this._responses, this._commands) {
+ _FiroCacheWorker._(this._responses, this._commands, this._isolate) {
_responses.listen(_handleResponsesFromIsolate);
}
@@ -93,6 +110,13 @@ class _FiroCacheWorker {
try {
final FCResult result;
switch (task.func) {
+ case FCFuncName._close:
+ setCacheDb.close();
+ usedTagsCacheDb.close();
+ receivePort.close();
+ sendPort.send((task.id, null));
+ return;
+
case FCFuncName._updateSparkAnonSetCoinsWith:
final data =
task.data as (SparkAnonymitySetMeta, List<RawSparkCoin>);
@@ -127,14 +151,8 @@ class _FiroCacheWorker {
final receivePort = ReceivePort();
args.$1.send(receivePort.sendPort);
final mutex = Mutex();
- final setCacheDb = sqlite3.open(
- args.$2,
- mode: OpenMode.readWrite,
- );
- final usedTagsCacheDb = sqlite3.open(
- args.$3,
- mode: OpenMode.readWrite,
- );
+ final setCacheDb = sqlite3.open(args.$2, mode: OpenMode.readWrite);
+ final usedTagsCacheDb = sqlite3.open(args.$3, mode: OpenMode.readWrite);
_handleCommandsToIsolate(
receivePort,
args.$1,
### lib/main.dart
@@ -40,7 +40,6 @@ import 'models/models.dart';
import 'models/node_model.dart';
import 'models/notification_model.dart';
import 'models/trade_wallet_lookup.dart';
-import 'pages/already_running_view.dart';
import 'pages/campfire_migrate_view.dart';
import 'pages/home_view/home_view.dart';
import 'pages/intro_view.dart';
@@ -64,9 +63,11 @@ import 'services/notifications_api.dart';
import 'services/notifications_service.dart';
import 'services/tor_service.dart';
import 'services/trade_service.dart';
+import 'themes/stack_theme_data.dart';
import 'themes/theme_providers.dart';
import 'themes/theme_service.dart';
import 'utilities/constants.dart';
+import 'utilities/desktop_startup_and_reset.dart';
import 'utilities/enums/backup_frequency_type.dart';
import 'utilities/flutter_secure_storage_interface.dart';
import 'utilities/logger.dart';
@@ -174,61 +175,37 @@ void main(List<String> args) async {
csWownero.setUseCsWowneroLoggerInternal(kDebugMode);
}
- DB.instance.hive.init(
- (await StackFileSystem.applicationHiveDirectory()).path,
- );
+ if (Util.isDesktop && !(await DesktopStartupAndReset.prepareForStartup())) {
+ return;
+ }
+
+ final hiveDir = await StackFileSystem.applicationHiveDirectory();
+ // Without any hive data this is a new install with nothing to migrate.
+ // Check before opening a box, which creates its file.
+ final isNewInstall =
+ Util.isDesktop &&
+ !hiveDir.listSync().any(
+ (file) => file.path.endsWith('.hive') || file.path.endsWith('.hivec'),
+ );
+ DB.instance.hive.init(hiveDir.path);
try {
await DB.instance.hive.openBox<dynamic>(DB.boxNameDBInfo);
await DB.instance.hive.openBox<dynamic>(DB.boxNamePrefs);
} on FileSystemException catch (e) {
- if (e.osError?.errorCode == 11 || e.message.contains('lock failed')) {
- // Another instance already holds the Hive database lock.
- // Try to bootstrap just enough of the theme system (Isar is independent
- // of Hive) so the error screen looks like a real Stack Wallet screen.
- Widget errorApp;
- try {
- await StackFileSystem.initThemesDir();
- await MainDB.instance.initMainDB();
- ThemeService.instance.init(MainDB.instance);
- errorApp = const ProviderScope(child: AlreadyRunningApp());
- } catch (_) {
- // Isar is also unavailable (e.g., another error). Fall back to a
- // minimal but still Inter-font styled screen.
- errorApp = MaterialApp(
- debugShowCheckedModeBanner: false,
- theme: ThemeData(fontFamily: GoogleFonts.inter().fontFamily),
- home: Scaffold(
- body: Center(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(
- AppConfig.appName,
- textAlign: TextAlign.center,
- style: GoogleFonts.inter(
- fontSize: 20,
- fontWeight: FontWeight.w600,
- ),
- ),
- const SizedBox(height: 8),
- Text(
- 'is already running.\n'
- 'Close the other window and try again.',
- textAlign: TextAlign.center,
- style: GoogleFonts.inter(fontSize: 16),
- ),
- ],
- ),
- ),
- ),
- );
- }
- runApp(errorApp);
+ if (DesktopStartupAndReset.isLockConflict(e)) {
+ await DesktopStartupAndReset.showAlreadyRunning();
return;
}
rethrow;
}
+ if (isNewInstall) {
+ await DB.instance.put<dynamic>(
+ boxName: DB.boxNameDBInfo,
+ key: 'hive_data_version',
+ value: Constants.currentDataVersion,
+ );
+ }
await Prefs.instance.init();
await Logging.instance.initialize(
@@ -758,13 +735,6 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
}
}
- InputBorder _buildOutlineInputBorder(Color color) {
- return OutlineInputBorder(
- borderSide: BorderSide(width: 1, color: color),
- borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius),
- );
- }
-
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
@@ -787,99 +757,7 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
navigatorKey: ref.read(pNavKey),
title: AppConfig.appName,
onGenerateRoute: RouteGenerator.generateRoute,
- theme: ThemeData(
- extensions: [colorScheme],
- highlightColor: colorScheme.highlight,
- brightness: colorScheme.brightness,
- fontFamily: GoogleFonts.inter().fontFamily,
- unselectedWidgetColor: colorScheme.radioButtonBorderDisabled,
- // textTheme: GoogleFonts.interTextTheme().copyWith(
- // button: STextStyles.button(context),
- // subtitle1: STextStyles.field(context).copyWith(
- // color: colorScheme.textDark,
- // ),
- // ),
- radioTheme: const RadioThemeData(
- splashRadius: 0,
- materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
- ),
- // splashFactory: NoSplash.splashFactory,
- splashColor: Colors.transparent,
- buttonTheme: ButtonThemeData(splashColor: colorScheme.splash),
- textButtonTheme: TextButtonThemeData(
- style: ButtonStyle(
- // splashFactory: NoSplash.splashFactory,
- overlayColor: MaterialStateProperty.all(colorScheme.splash),
- minimumSize: MaterialStateProperty.all<Size>(const Size(46, 46)),
- // textStyle: MaterialStateProperty.all<TextStyle>(
- // STextStyles.button(context)),
- foregroundColor: MaterialStateProperty.all(
- colorScheme.buttonTextSecondary,
- ),
- backgroundColor: MaterialStateProperty.all<Color>(
- colorScheme.buttonBackSecondary,
- ),
- shape: MaterialStateProperty.all<OutlinedBorder>(
- RoundedRectangleBorder(
- // 1000 to be relatively sure it keeps its pill shape
- borderRadius: BorderRadius.circular(1000),
- ),
- ),
- ),
- ),
- primaryColor: colorScheme.accentColorDark,
- primarySwatch: Util.createMaterialColor(colorScheme.accentColorDark),
- checkboxTheme: CheckboxThemeData(
- splashRadius: 0,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(
- Constants.size.checkboxBorderRadius,
- ),
- ),
- checkColor: MaterialStateColor.resolveWith((state) {
- if (state.contains(MaterialState.selected)) {
- return colorScheme.checkboxIconChecked;
- }
- return colorScheme.checkboxBGChecked;
- }),
- fillColor: MaterialStateColor.resolveWith((states) {
- if (states.contains(MaterialState.selected)) {
- return colorScheme.checkboxBGChecked;
- }
- return colorScheme.checkboxBorderEmpty;
- }),
- ),
- appBarTheme: AppBarTheme(
- centerTitle: false,
- color: colorScheme.background,
- surfaceTintColor: colorScheme.background,
- elevation: 0,
- ),
- inputDecorationTheme: InputDecorationTheme(
- focusColor: colorScheme.textFieldDefaultBG,
- fillColor: colorScheme.textFieldDefaultBG,
- filled: true,
- contentPadding: const EdgeInsets.symmetric(
- vertical: 6,
- horizontal: 12,
- ),
- // labelStyle: STextStyles.fieldLabel(context),
- // hintStyle: STextStyles.fieldLabel(context),
- enabledBorder: _buildOutlineInputBorder(
- colorScheme.textFieldDefaultBG,
- ),
- focusedBorder: _buildOutlineInputBorder(
- colorScheme.textFieldDefaultBG,
- ),
- errorBorder: _buildOutlineInputBorder(colorScheme.textFieldDefaultBG),
- disabledBorder: _buildOutlineInputBorder(
- colorScheme.textFieldDefaultBG,
- ),
- focusedErrorBorder: _buildOutlineInputBorder(
- colorScheme.textFieldDefaultBG,
- ),
- ),
- ),
+ theme: stackThemeData(colorScheme),
home: CryptoNotifications(
child: Util.isDesktop
? FutureBuilder(
### lib/pages/already_running_view.dart
@@ -13,61 +13,15 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
-import 'package:google_fonts/google_fonts.dart';
import '../app_config.dart';
import '../themes/stack_colors.dart';
import '../themes/theme_providers.dart';
-import '../themes/theme_service.dart';
-import '../utilities/stack_file_system.dart';
import '../utilities/text_styles.dart';
import '../utilities/util.dart';
import '../widgets/app_icon.dart';
import '../widgets/background.dart';
-/// Root app widget for the "already running" error path.
-///
-/// Mirrors the theme bootstrap performed by [MaterialAppWithTheme] in main.dart
-/// but without touching Hive. Requires Isar + ThemeService to already be
-/// initialized before [runApp] is called.
-class AlreadyRunningApp extends ConsumerStatefulWidget {
- const AlreadyRunningApp({super.key});
-
- @override
- ConsumerState<AlreadyRunningApp> createState() => _AlreadyRunningAppState();
-}
-
-class _AlreadyRunningAppState extends ConsumerState<AlreadyRunningApp> {
- @override
- void initState() {
- super.initState();
- WidgetsBinding.instance.addPostFrameCallback((_) {
- ref.read(applicationThemesDirectoryPathProvider.notifier).state =
- StackFileSystem.themesDir!.path;
- // The first instance already verified/installed the light theme, so
- // getTheme cannot return null here.
- ref.read(themeProvider.state).state = ref
- .read(pThemeService)
- .getTheme(themeId: "light")!;
- });
- }
-
- @override
- Widget build(BuildContext context) {
- final colorScheme = ref.watch(colorProvider.state).state;
- return MaterialApp(
- debugShowCheckedModeBanner: false,
- title: AppConfig.appName,
- theme: ThemeData(
- extensions: [colorScheme],
- fontFamily: GoogleFonts.inter().fontFamily,
- splashColor: Colors.transparent,
- ),
- home: const AlreadyRunningView(),
- );
- }
-}
-
/// Error screen shown when this is a second instance of the app.
///
/// Mirrors [IntroView]'s layout: themed background, logo, app name heading,
@@ -159,25 +113,22 @@ class AlreadyRunningView extends ConsumerWidget {
Text(
AppConfig.appName,
textAlign: TextAlign.center,
- style: STextStyles.pageTitleH1(
- context,
- ).copyWith(fontSize: 40),
+ style: STextStyles.pageTitleH1(context)
+ .copyWith(fontSize: 40),
),
const Spacer(flex: 24),
Text(
AppConfig.shortDescriptionText,
textAlign: TextAlign.center,
- style: STextStyles.subtitle(
- context,
- ).copyWith(fontSize: 24),
+ style: STextStyles.subtitle(context)
+ .copyWith(fontSize: 24),
),
const Spacer(flex: 42),
Text(
_errorMessage,
textAlign: TextAlign.center,
- style: STextStyles.label(
- context,
- ).copyWith(fontSize: 18),
+ style: STextStyles.label(context)
+ .copyWith(fontSize: 18),
),
const Spacer(flex: 65),
],
### lib/pages/send_view/send_view.dart
@@ -169,6 +169,16 @@ class _SendViewState extends ConsumerState<SendView> {
noteController.text = paymentData.label!;
}
+ // firo-qt treats message as the spark memo when paying a spark address
+ if (coin is Firo &&
+ paymentData.message != null &&
+ SparkInterface.validateSparkAddress(
+ address: _address!,
+ isTestNet: coin.network.isTestNet,
+ )) {
+ memoController.text = paymentData.message!;
+ }
+
// autofill amount field
if (paymentData.amount != null) {
final amount = Amount.tryParseCanonicalAmount(
### lib/pages/signing/signing_view.dart
@@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import '../../providers/global/wallets_provider.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
+import '../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../widgets/background.dart';
import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
@@ -31,35 +33,38 @@ class _SigningViewState extends ConsumerState<SigningView> {
// keep auto dispose providers alive
ref.listen(pSignIsValid, (_, __) {});
ref.listen(pVerifyIsValid, (_, __) {});
+ final wallet = ref.watch(pWallets).getWallet(widget.walletId);
+ final viewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly;
return ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
- backgroundColor: Theme.of(
- context,
- ).extension<StackColors>()!.background,
+ backgroundColor: Theme.of(context)
+ .extension<StackColors>()!
+ .background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
- "Sign / Verify",
+ viewOnly ? "Verify message" : "Sign / Verify",
style: STextStyles.navBarTitle(context),
),
),
- body: SafeArea(child: child),
+ body: SafeArea(child: SingleChildScrollView(child: child)),
),
),
child: CustomTabView(
- titles: const ["Sign message", "Verify message"],
+ titles: [if (!viewOnly) "Sign message", "Verify message"],
children: [
- SignMessageForm(
- key: const Key("_SignMessageFormKey"),
- walletId: widget.walletId,
- ),
+ if (!viewOnly)
+ SignMessageForm(
+ key: const Key("_SignMessageFormKey"),
+ walletId: widget.walletId,
+ ),
VerifyMessageForm(
key: const Key("_VerifyMessageFormKey"),
walletId: widget.walletId,
### lib/pages/signing/sub_widgets/sign_message_tab.dart
@@ -19,6 +19,8 @@ import '../../../widgets/detail_item.dart';
import '../../../widgets/dialogs/s_dialog.dart';
import '../../../widgets/rounded_container.dart';
import '../../../widgets/textfields/adaptive_text_field.dart';
+import '../../wallet_view/transaction_views/transaction_details_view.dart'
+ show IconCopyButton;
import '../signing_view.dart';
import 'address_list.dart';
@@ -32,7 +34,7 @@ final class _SignState {
required this.signature,
});
- bool get isValid => message.isNotEmpty && address != null;
+ bool get isValid => message.trim().isNotEmpty && address != null;
_SignState copyWith({String? message, String? signature}) {
return _SignState(
@@ -77,9 +79,9 @@ class _SignMessageFormState extends ConsumerState<SignMessageForm> {
TextStyle _getStyle(BuildContext context) {
return Util.isDesktop
? STextStyles.desktopTextExtraExtraSmall(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textFieldActiveSearchIconRight,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .textFieldActiveSearchIconRight,
)
: STextStyles.smallMed12(context);
}
@@ -133,9 +135,9 @@ class _SignMessageFormState extends ConsumerState<SignMessageForm> {
child: RoundedContainer(
padding: EdgeInsets.zero,
color: Colors.transparent,
- borderColor: Theme.of(
- context,
- ).extension<StackColors>()!.textFieldDefaultBG,
+ borderColor: Theme.of(context)
+ .extension<StackColors>()!
+ .textFieldDefaultBG,
child: child,
),
),
@@ -181,9 +183,11 @@ class _SignMessageFormState extends ConsumerState<SignMessageForm> {
onException: (e) => ex = e,
);
- if (mounted && ex != null) {
+ if (!mounted || !identical(ref.read(_pSignState), state)) return;
+
+ if (ex != null) {
await showSignVerifyError(ex!, context: context);
- } else if (signature != null && mounted) {
+ } else if (signature != null) {
ref.read(_pSignState.notifier).state = state.copyWith(
signature: signature,
);
@@ -214,7 +218,13 @@ class _SignMessageFormState extends ConsumerState<SignMessageForm> {
AdaptiveTextField(
controller: messageController,
showPasteClearButton: true,
- maxLines: 1,
+ trimPastedText: false,
+ autocorrect: false,
+ smartDashesType: SmartDashesType.disabled,
+ smartQuotesType: SmartQuotesType.disabled,
+ enableSuggestions: false,
+ minLines: 1,
+ maxLines: 5,
onChangedComprehensive: (_) {
if (mounted) {
ref.read(_pSignState.notifier).state = ref
@@ -249,6 +259,8 @@ class _SignMessageFormState extends ConsumerState<SignMessageForm> {
noPadding: Util.isDesktop,
button: ref.watch(_pSignState.select((s) => s.signature)).isEmpty
? null
+ : Util.isDesktop
+ ? IconCopyButton(data: ref.read(_pSignState).signature)
: SimpleCopyButton(data: ref.read(_pSignState).signature),
),
### lib/pages/signing/sub_widgets/verify_message_tab.dart
@@ -67,9 +67,9 @@ class _VerifyMessageFormState extends ConsumerState<VerifyMessageForm> {
TextStyle _getStyle(BuildContext context) {
return Util.isDesktop
? STextStyles.desktopTextExtraExtraSmall(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textFieldActiveSearchIconRight,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .textFieldActiveSearchIconRight,
)
: STextStyles.smallMed12(context);
}
@@ -84,6 +84,7 @@ class _VerifyMessageFormState extends ConsumerState<VerifyMessageForm> {
_verify = IfNotAlreadyAsync<void>(() async {
Exception? ex;
+ final state = ref.read(_pVerifyState);
final verified = await showLoading(
whileFuture:
@@ -100,7 +101,7 @@ class _VerifyMessageFormState extends ConsumerState<VerifyMessageForm> {
onException: (e) => ex = e,
);
- if (mounted) {
+ if (mounted && identical(ref.read(_pVerifyState), state)) {
if (ex != null) {
await showSignVerifyError(ex!, context: context);
} else {
@@ -145,7 +146,13 @@ class _VerifyMessageFormState extends ConsumerState<VerifyMessageForm> {
AdaptiveTextField(
controller: messageController,
showPasteClearButton: true,
- maxLines: 1,
+ trimPastedText: false,
+ autocorrect: false,
+ smartDashesType: SmartDashesType.disabled,
+ smartQuotesType: SmartQuotesType.disabled,
+ enableSuggestions: false,
+ minLines: 1,
+ maxLines: 5,
onChangedComprehensive: (_) {
if (mounted) {
ref.read(_pVerifyState.notifier).state = ref
### lib/pages/spark_names/sub_widgets/spark_name_details.dart
@@ -7,6 +7,7 @@ import '../../../providers/db/drift_provider.dart';
import '../../../providers/db/main_db_provider.dart';
import '../../../providers/global/wallets_provider.dart';
import '../../../themes/stack_colors.dart';
+import '../../../utilities/show_loading.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../wallets/isar/providers/wallet_info_provider.dart';
@@ -19,6 +20,8 @@ import '../../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../../widgets/desktop/primary_button.dart';
import '../../../widgets/dialogs/s_dialog.dart';
import '../../../widgets/rounded_container.dart';
+import '../../../widgets/textfields/adaptive_text_field.dart';
+import '../../signing/signing_view.dart';
import '../../wallet_view/transaction_views/transaction_details_view.dart'
as tvd;
import '../buy_spark_name_view.dart';
@@ -131,6 +134,34 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
}
}
+ Future<void> _proveOwnership() async {
+ if (_lock) return;
+ _lock = true;
+ try {
+ if (Util.isDesktop) {
+ await showDialog<void>(
+ context: context,
+ builder: (context) => Padding(
+ padding: MediaQuery.viewInsetsOf(context),
+ child: SDialog(
+ child: SparkAddressOwnershipProofView(
+ walletId: widget.walletId,
+ address: name.address,
+ ),
+ ),
+ ),
+ );
+ } else {
+ await Navigator.of(context).pushNamed(
+ SparkAddressOwnershipProofView.routeName,
+ arguments: (walletId: widget.walletId, address: name.address),
+ );
+ }
+ } finally {
+ _lock = false;
+ }
+ }
+
@override
void initState() {
super.initState();
@@ -227,9 +258,9 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
child: RoundedContainer(
padding: EdgeInsets.zero,
color: Colors.transparent,
- borderColor: Theme.of(
- context,
- ).extension<StackColors>()!.textFieldDefaultBG,
+ borderColor: Theme.of(context)
+ .extension<StackColors>()!
+ .textFieldDefaultBG,
child: child,
),
),
@@ -283,9 +314,9 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
Text(
"Address",
style: STextStyles.w500_14(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textSubtitle1,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .textSubtitle1,
),
),
Util.isDesktop
@@ -320,9 +351,9 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
: const EdgeInsets.all(12),
color: Util.isDesktop
? Colors.transparent
- : Theme.of(
- context,
- ).extension<StackColors>()!.popupBG,
+ : Theme.of(context)
+ .extension<StackColors>()!
+ .popupBG,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
@@ -382,17 +413,16 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
Text(
"Expiry",
style: STextStyles.w500_14(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textSubtitle1,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .textSubtitle1,
),
),
const SizedBox(height: 4),
SelectableText(
message,
- style: STextStyles.w500_14(
- context,
- ).copyWith(color: color),
+ style: STextStyles.w500_14(context)
+ .copyWith(color: color),
),
],
),
@@ -422,9 +452,9 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
Text(
"Additional info",
style: STextStyles.w500_14(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textSubtitle1,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .textSubtitle1,
),
),
const SizedBox(height: 4),
@@ -435,6 +465,32 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
],
),
),
+ if (!_isViewOnlyWallet) ...[
+ const _Div(),
+ Padding(
+ padding: Util.isDesktop
+ ? const EdgeInsets.all(16)
+ : EdgeInsets.zero,
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight: Util.isDesktop ? 70 : 48,
+ ),
+ child: TextButton(
+ style: Theme.of(context)
+ .extension<StackColors>()!
+ .getPrimaryEnabledButtonStyle(context),
+ onPressed: _proveOwnership,
+ child: Text(
+ "Prove address ownership",
+ textAlign: TextAlign.center,
+ style: Util.isDesktop
+ ? STextStyles.desktopButtonEnabled(context)
+ : STextStyles.button(context),
+ ),
+ ),
+ ),
+ ),
+ ],
],
);
},
@@ -444,6 +500,174 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
}
}
+class SparkAddressOwnershipProofView extends ConsumerStatefulWidget {
+ const SparkAddressOwnershipProofView({
+ super.key,
+ required this.walletId,
+ required this.address,
+ });
+
+ final String walletId;
+ final String address;
+
+ static const routeName = "/sparkAddressOwnershipProof";
+
+ @override
+ ConsumerState<SparkAddressOwnershipProofView> createState() =>
+ _SparkAddressOwnershipProofViewState();
+}
+
+class _SparkAddressOwnershipProofViewState
+ extends ConsumerState<SparkAddressOwnershipProofView> {
+ final _messageController = TextEditingController();
+
+ String _proof = "";
+ bool _isGenerating = false;
+
+ @override
+ void dispose() {
+ _messageController.dispose();
+ super.dispose();
+ }
+
+ void _onMessageChanged(String message) {
+ setState(() {
+ _proof = "";
+ });
+ }
+
+ Future<void> _generateProof() async {
+ if (_isGenerating || _messageController.text.trim().isEmpty) {
+ return;
+ }
+
+ final message = _messageController.text;
+ setState(() => _isGenerating = true);
+ Exception? exception;
+ final proof = await showLoading(
+ whileFuture:
+ (ref.read(pWallets).getWallet(widget.walletId) as SparkInterface)
+ .createSparkAddressOwnershipProof(
+ address: widget.address,
+ message: message,
+ ),
+ context: context,
+ message: "Creating proof...",
+ onException: (e) => exception = e,
+ );
+
+ if (!mounted) return;
+ setState(() => _isGenerating = false);
+
+ if (exception != null) {
+ await showSignVerifyError(exception!, context: context);
+ } else if (proof != null && _messageController.text == message) {
+ setState(() => _proof = proof);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final canGenerate =
+ !_isGenerating && _messageController.text.trim().isNotEmpty;
+
+ return ConditionalParent(
+ condition: !Util.isDesktop,
+ builder: (child) => Background(
+ child: Scaffold(
+ backgroundColor: Colors.transparent,
+ appBar: AppBar(
+ leading: const AppBarBackButton(),
+ title: Text(
+ "Prove address ownership",
+ style: STextStyles.navBarTitle(context),
+ ),
+ ),
+ body: SafeArea(child: SingleChildScrollView(child: child)),
+ ),
+ ),
+ child: SizedBox(
+ width: Util.isDesktop ? 580 : null,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ if (Util.isDesktop)
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.only(left: 32),
+ child: Text(
+ "Prove address ownership",
+ style: STextStyles.desktopH3(context),
+ ),
+ ),
+ ),
+ const DesktopDialogCloseButton(),
+ ],
+ ),
+ Padding(
+ padding: EdgeInsets.fromLTRB(
+ Util.isDesktop ? 32 : 16,
+ Util.isDesktop ? 10 : 16,
+ Util.isDesktop ? 32 : 16,
+ Util.isDesktop ? 32 : 16,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text("Spark address", style: STextStyles.w500_14(context)),
+ const SizedBox(height: 8),
+ SelectableText(
+ widget.address,
+ style: STextStyles.w500_14(context),
+ ),
+ const SizedBox(height: 20),
+ Text("Message", style: STextStyles.w500_14(context)),
+ const SizedBox(height: 8),
+ AdaptiveTextField(
+ controller: _messageController,
+ minLines: 3,
+ maxLines: 5,
+ autocorrect: false,
+ smartDashesType: SmartDashesType.disabled,
+ smartQuotesType: SmartQuotesType.disabled,
+ enableSuggestions: false,
+ readOnly: _isGenerating,
+ onChangedComprehensive: _onMessageChanged,
+ ),
+ if (_proof.isNotEmpty) ...[
+ const SizedBox(height: 20),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text("Proof", style: STextStyles.w500_14(context)),
+ Util.isDesktop
+ ? tvd.IconCopyButton(data: _proof)
+ : SimpleCopyButton(data: _proof),
+ ],
+ ),
+ const SizedBox(height: 8),
+ SelectableText(_proof, style: STextStyles.w500_14(context)),
+ ],
+ const SizedBox(height: 24),
+ PrimaryButton(
+ label: "Create proof",
+ enabled: canGenerate,
+ onPressed: canGenerate ? _generateProof : null,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
class _Div extends StatelessWidget {
const _Div({super.key});
### lib/pages/wallet_view/wallet_view.dart
@@ -446,9 +446,8 @@ class _WalletViewState extends ConsumerState<WalletView> {
if (publicBalance <= Amount.zero) {
shouldPop = true;
if (mounted) {
- Navigator.of(
- context,
- ).popUntil(ModalRoute.withName(WalletView.routeName));
+ Navigator.of(context)
+ .popUntil(ModalRoute.withName(WalletView.routeName));
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
@@ -468,9 +467,8 @@ class _WalletViewState extends ConsumerState<WalletView> {
}
shouldPop = true;
if (mounted) {
- Navigator.of(
- context,
- ).popUntil(ModalRoute.withName(WalletView.routeName));
+ Navigator.of(context)
+ .popUntil(ModalRoute.withName(WalletView.routeName));
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
@@ -482,9 +480,8 @@ class _WalletViewState extends ConsumerState<WalletView> {
} catch (e) {
shouldPop = true;
if (mounted) {
- Navigator.of(
- context,
- ).popUntil(ModalRoute.withName(WalletView.routeName));
+ Navigator.of(context)
+ .popUntil(ModalRoute.withName(WalletView.routeName));
await showDialog<dynamic>(
context: context,
builder: (_) => StackOkDialog(
@@ -519,13 +516,12 @@ class _WalletViewState extends ConsumerState<WalletView> {
child,
Background(
child: CustomLoadingOverlay(
- message:
- "Migration in progress\nThis could take a while\nPlease don't leave this screen",
+ message: "Migration in progress\nThis could take a while\nPlease don't leave this screen",
subMessage: "This only needs to run once per wallet",
eventBus: null,
- textColor: Theme.of(
- context,
- ).extension<StackColors>()!.textDark,
+ textColor: Theme.of(context)
+ .extension<StackColors>()!
+ .textDark,
actionButton: SecondaryButton(
label: "Cancel",
onPressed: () async {
@@ -568,9 +564,9 @@ class _WalletViewState extends ConsumerState<WalletView> {
child: Stack(
children: [
Scaffold(
- backgroundColor: Theme.of(
- context,
- ).extension<StackColors>()!.background,
+ backgroundColor: Theme.of(context)
+ .extension<StackColors>()!
+ .background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
@@ -623,9 +619,9 @@ class _WalletViewState extends ConsumerState<WalletView> {
key: const Key("walletViewRadioButton"),
size: 36,
shadows: const [],
- color: Theme.of(
- context,
- ).extension<StackColors>()!.background,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .background,
icon: _buildNetworkIcon(_currentSyncStatus),
onPressed: () {
Navigator.of(context).pushNamed(
@@ -649,14 +645,13 @@ class _WalletViewState extends ConsumerState<WalletView> {
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
- semanticsLabel:
- "Notifications Button. Takes To Notifications Page.",
+ semanticsLabel: "Notifications Button. Takes To Notifications Page.",
key: const Key("walletViewAlertsButton"),
size: 36,
shadows: const [],
- color: Theme.of(
- context,
- ).extension<StackColors>()!.background,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .background,
icon:
ref.watch(
notificationsProvider.select(
@@ -768,14 +763,14 @@ class _WalletViewState extends ConsumerState<WalletView> {
key: const Key("walletViewSettingsButton"),
size: 36,
shadows: const [],
- color: Theme.of(
- context,
- ).extension<StackColors>()!.background,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .background,
icon: SvgPicture.asset(
Assets.svg.bars,
- color: Theme.of(
- context,
- ).extension<StackColors>()!.accentColorDark,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .accentColorDark,
width: 20,
height: 20,
),
@@ -799,9 +794,9 @@ class _WalletViewState extends ConsumerState<WalletView> {
),
body: SafeArea(
child: Container(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.background,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .background,
child: Column(
children: [
const SizedBox(height: 10),
@@ -849,8 +844,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
context: context,
builder: (context) => StackDialog(
title: "Attention!",
- message:
- "You're about to privatize all of your public funds.",
+ message: "You're about to privatize all of your public funds.",
leftButton: TextButton(
onPressed: () {
Navigator.of(context).pop();
@@ -912,9 +906,9 @@ class _WalletViewState extends ConsumerState<WalletView> {
"Transactions",
style: STextStyles.itemSubtitle(context)
.copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textDark3,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .textDark3,
),
),
CustomTextButton(
@@ -1139,9 +1133,9 @@ class _WalletViewState extends ConsumerState<WalletView> {
Assets.svg.monkey,
height: 20,
width: 20,
- color: Theme.of(
- context,
- ).extension<StackColors>()!.bottomNavIconIcon,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .bottomNavIconIcon,
),
label: "MonKey",
onTap: () {
@@ -1151,17 +1145,18 @@ class _WalletViewState extends ConsumerState<WalletView> {
);
},
),
- if (wallet is SignVerifyInterface && !viewOnly)
+ if (wallet is SignVerifyInterface &&
+ (!viewOnly || wallet is SparkInterface))
WalletNavigationBarItemData(
icon: SvgPicture.asset(
Assets.svg.pencil,
height: 20,
width: 20,
- color: Theme.of(
- context,
- ).extension<StackColors>()!.bottomNavIconIcon,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .bottomNavIconIcon,
),
- label: "Sign/Verify",
+ label: viewOnly ? "Verify message" : "Sign/Verify",
onTap: () {
Navigator.of(context).pushNamed(
SigningView.routeName,
@@ -1213,9 +1208,9 @@ class _WalletViewState extends ConsumerState<WalletView> {
height: 20,
width: 20,
colorFilter: ColorFilter.mode(
- Theme.of(
- context,
- ).extension<StackColors>()!.bottomNavIconIcon,
+ Theme.of(context)
+ .extension<StackColors>()!
+ .bottomNavIconIcon,
BlendMode.srcIn,
),
),
### lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
@@ -1010,6 +1010,16 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
_note = paymentData.label;
}
+ // firo-qt treats message as the spark memo when paying a spark address
+ if (coin is Firo &&
+ paymentData.message != null &&
+ SparkInterface.validateSparkAddress(
+ address: _address!,
+ isTestNet: coin.network.isTestNet,
+ )) {
+ memoController.text = paymentData.message!;
+ }
+
// autofill amount field
if (paymentData.amount != null) {
final amount = Amount.tryParseCanonicalAmount(
### lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart
@@ -149,15 +149,13 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
}
void _onSparkCoinsPressed() {
- Navigator.of(
- context,
- ).pushNamed(SparkCoinsView.routeName, arguments: widget.walletId);
+ Navigator.of(context)
+ .pushNamed(SparkCoinsView.routeName, arguments: widget.walletId);
}
void _onMwebUtxosPressed() {
- Navigator.of(
- context,
- ).pushNamed(MwebUtxosView.routeName, arguments: widget.walletId);
+ Navigator.of(context)
+ .pushNamed(MwebUtxosView.routeName, arguments: widget.walletId);
}
Future<void> _onAnonymizeAllPressed() async {
@@ -230,9 +228,8 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
shouldPop = true;
if (context.mounted) {
Navigator.of(context, rootNavigator: true).pop();
- Navigator.of(
- context,
- ).popUntil(ModalRoute.withName(DesktopWalletView.routeName));
+ Navigator.of(context)
+ .popUntil(ModalRoute.withName(DesktopWalletView.routeName));
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
@@ -253,9 +250,8 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
shouldPop = true;
if (mounted) {
Navigator.of(context, rootNavigator: true).pop();
- Navigator.of(
- context,
- ).popUntil(ModalRoute.withName(DesktopWalletView.routeName));
+ Navigator.of(context)
+ .popUntil(ModalRoute.withName(DesktopWalletView.routeName));
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
@@ -268,9 +264,8 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
shouldPop = true;
if (mounted) {
Navigator.of(context, rootNavigator: true).pop();
- Navigator.of(
- context,
- ).popUntil(ModalRoute.withName(DesktopWalletView.routeName));
+ Navigator.of(context)
+ .popUntil(ModalRoute.withName(DesktopWalletView.routeName));
await showDialog<dynamic>(
context: context,
builder: (_) => DesktopDialog(
@@ -345,51 +340,43 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
) {
ref.read(myPaynymAccountStateProvider.state).state = account.value!;
- await Navigator.of(
- context,
- ).pushNamed(PaynymHomeView.routeName, arguments: widget.walletId);
+ await Navigator.of(context)
+ .pushNamed(PaynymHomeView.routeName, arguments: widget.walletId);
} else {
- await Navigator.of(
- context,
- ).pushNamed(PaynymClaimView.routeName, arguments: widget.walletId);
+ await Navigator.of(context)
+ .pushNamed(PaynymClaimView.routeName, arguments: widget.walletId);
}
}
}
Future<void> _onMonkeyPressed() async {
- await (Navigator.of(
- context,
- ).pushNamed(MonkeyView.routeName, arguments: widget.walletId));
+ await (Navigator.of(context)
+ .pushNamed(MonkeyView.routeName, arguments: widget.walletId));
}
void _onOrdinalsPressed() {
- Navigator.of(
- context,
- ).pushNamed(DesktopOrdinalsView.routeName, arguments: widget.walletId);
+ Navigator.of(context)
+ .pushNamed(DesktopOrdinalsView.routeName, arguments: widget.walletId);
}
void _onFusionPressed() {
- Navigator.of(
- context,
- ).pushNamed(DesktopCashFusionView.routeName, arguments: widget.walletId);
+ Navigator.of(context)
+ .pushNamed(DesktopCashFusionView.routeName, arguments: widget.walletId);
}
void _onChurnPressed() {
- Navigator.of(
- context,
- ).pushNamed(DesktopChurningView.routeName, arguments: widget.walletId);
+ Navigator.of(context)
+ .pushNamed(DesktopChurningView.routeName, arguments: widget.walletId);
}
void _onNamesPressed() {
- Navigator.of(
- context,
- ).pushNamed(NamecoinNamesHomeView.routeName, arguments: widget.walletId);
+ Navigator.of(context)
+ .pushNamed(NamecoinNamesHomeView.routeName, arguments: widget.walletId);
}
void _onSparkNamesPressed() {
- Navigator.of(
- context,
- ).pushNamed(SparkNamesHomeView.routeName, arguments: widget.walletId);
+ Navigator.of(context)
+ .pushNamed(SparkNamesHomeView.routeName, arguments: widget.walletId);
}
Future<void> _onSalviumStakePressed() async {
@@ -430,8 +417,9 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
context: context,
builder: (context) => DesktopDialog(
maxWidth: 580,
- maxHeight: double.infinity,
+ maxHeight: null,
child: Column(
+ mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -446,9 +434,11 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
const DesktopDialogCloseButton(),
],
),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 32),
- child: SigningView(walletId: widget.walletId),
+ Flexible(
+ child: SingleChildScrollView(
+ padding: const EdgeInsets.symmetric(horizontal: 32),
+ child: SigningView(walletId: widget.walletId),
+ ),
),
const SizedBox(height: 32),
],
@@ -458,9 +448,8 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
}
void _onMasternodesPressed() {
- Navigator.of(
- context,
- ).pushNamed(MasternodesHomeView.routeName, arguments: widget.walletId);
+ Navigator.of(context)
+ .pushNamed(MasternodesHomeView.routeName, arguments: widget.walletId);
}
List<(WalletFeature, String, FutureOr<void> Function())> _getOptions(
@@ -502,7 +491,8 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
_onSalviumStakePressed,
),
- if (wallet is SignVerifyInterface && !isViewOnly)
+ if (wallet is SignVerifyInterface &&
+ (!isViewOnly || wallet is SparkInterface))
(WalletFeature.sign, Assets.svg.pencil, _onSignPressed),
if (!isViewOnly && wallet is FiroWallet)
@@ -619,9 +609,9 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
Assets.svg.bars,
height: 20,
width: 20,
- color: Theme.of(
- context,
- ).extension<StackColors>()!.buttonTextSecondary,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .buttonTextSecondary,
),
onPressed: () => _onMorePressed([
...options.sublist(options.length - count),
@@ -648,17 +638,17 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
),
height: 20,
width: 20,
- color: Theme.of(
- context,
- ).extension<StackColors>()!.buttonTextSecondary,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .buttonTextSecondary,
)
: SvgPicture.asset(
option.$2,
height: 20,
width: 20,
- color: Theme.of(
- context,
- ).extension<StackColors>()!.buttonTextSecondary,
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .buttonTextSecondary,
),
onPressed: () => option.$3(),
),
### lib/pages_desktop_specific/password/delete_password_warning_view.dart
@@ -8,19 +8,15 @@
*
*/
-import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:isar_community/isar.dart';
import '../../app_config.dart';
-import '../../db/hive/db.dart';
-import '../../notifications/show_flush_bar.dart';
-import '../../pages/intro_view.dart';
+import '../../themes/theme_providers.dart';
+import '../../utilities/desktop_startup_and_reset.dart';
import '../../utilities/logger.dart';
-import '../../utilities/stack_file_system.dart';
import '../../utilities/text_styles.dart';
import '../../widgets/app_icon.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
@@ -45,67 +41,30 @@ class _ForgotPasswordDesktopViewState
extends ConsumerState<DeletePasswordWarningView> {
bool _deleteInProgress = false;
- Future<bool> _deleteStack() async {
- final appRoot = await StackFileSystem.applicationRootDirectory();
-
+ Future<void> _deleteStack() async {
+ // Dispose the normal UI before closing the databases it depends on.
+ await DesktopStartupAndReset.showMessage(
+ 'Resetting ${AppConfig.appName}...',
+ theme: ref.read(themeProvider),
+ );
+ await WidgetsBinding.instance.endOfFrame;
try {
- await DB.instance.hive.close();
- if (Platform.isWindows) {
- final xmrDir = Directory("${appRoot.path}/wallets");
- if (xmrDir.existsSync()) {
- await xmrDir.delete(recursive: true);
- }
- final epicDir = Directory("${appRoot.path}/epiccash");
- if (epicDir.existsSync()) {
- await epicDir.delete(recursive: true);
- }
-
- final mimblewimblecoinDir = Directory(
- "${appRoot.path}/mimblewimblecoin",
- );
- if (mimblewimblecoinDir.existsSync()) {
- await mimblewimblecoinDir.delete(recursive: true);
- }
-
- await Isar.getInstance("desktopStore")?.close(deleteFromDisk: true);
-
- await (await StackFileSystem.applicationHiveDirectory()).delete(
- recursive: true,
- );
- } else if (Platform.isLinux) {
- await appRoot.delete(recursive: true);
- } else {
- // macos in ipad mode
- final xmrDir = Directory("${appRoot.path}/wallets");
- if (xmrDir.existsSync()) {
- await xmrDir.delete(recursive: true);
- }
- final epicDir = Directory("${appRoot.path}/epiccash");
- if (epicDir.existsSync()) {
- await epicDir.delete(recursive: true);
- }
- final mimblewimblecoinDir = Directory(
- "${appRoot.path}/mimblewimblecoin",
- );
- if (mimblewimblecoinDir.existsSync()) {
- await mimblewimblecoinDir.delete(recursive: true);
- }
-
- await (await StackFileSystem.applicationHiveDirectory()).delete(
- recursive: true,
- );
- await (await StackFileSystem.applicationIsarDirectory()).delete(
- recursive: true,
- );
- }
-
- await DB.instance.init();
+ await DesktopStartupAndReset.resetBeforeExit();
} catch (e, s) {
- Logging.instance.f("$e\n$s", error: e, stackTrace: s);
- return false;
+ Logging.instance.f('$e\n$s', error: e, stackTrace: s);
+ await DesktopStartupAndReset.showMessage(
+ 'Reset could not finish. ${AppConfig.appName} will now close.\n'
+ 'Reopen the app to try again.',
+ );
+ await Future<void>.delayed(const Duration(seconds: 3));
+ exit(1);
}
-
- return true;
+ await DesktopStartupAndReset.showMessage(
+ '${AppConfig.appName} was reset and will now close.\n'
+ 'Reopen it to create or restore your wallets.',
+ );
+ await Future<void>.delayed(const Duration(seconds: 3));
+ exit(0);
}
@override
@@ -145,23 +104,28 @@ class _ForgotPasswordDesktopViewState
style: STextStyles.desktopTextSmall(context),
),
TextSpan(
- text:
- widget.shouldCreateNew
- ? "create a new ${AppConfig.prefix}"
- : "restore from backup",
+ text: widget.shouldCreateNew
+ ? "create a new ${AppConfig.prefix}"
+ : "restore from backup",
style: STextStyles.desktopTextSmallBold(context),
),
TextSpan(
text: ", we need to ",
style: STextStyles.desktopTextSmall(context),
),
TextSpan(
- text: "delete your old wallets",
+ text: "delete all wallets and app data",
style: STextStyles.desktopTextSmallBold(context),
),
TextSpan(
text:
- ". All wallets will be lost. If you have not written down your recovery phrase for EACH wallet, you may be in danger of losing funds. Continue?",
+ ". This includes contacts, notes, swap "
+ "history and settings. Files ending in .swb "
+ "and logs are not deleted. Without the recovery "
+ "phrase or a backup for EACH wallet, its "
+ "funds will be lost. ${AppConfig.appName} "
+ "will close; reopen it to create or restore "
+ "your wallets. Continue?",
style: STextStyles.desktopTextSmall(context),
),
],
@@ -170,49 +134,12 @@ class _ForgotPasswordDesktopViewState
),
const SizedBox(height: 48),
PrimaryButton(
- label: "Delete and continue",
+ label: "Delete everything and quit",
enabled: !_deleteInProgress,
onPressed: () async {
- final shouldDelete = !_deleteInProgress;
- setState(() {
- _deleteInProgress = true;
- });
-
- if (shouldDelete) {
- unawaited(
- showFloatingFlushBar(
- type: FlushBarType.info,
- message: "Deleting wallet...",
- context: context,
- ),
- );
-
- final success = await _deleteStack();
-
- if (success) {
- await showFloatingFlushBar(
- type: FlushBarType.success,
- message: "Wallet deleted",
- context: context,
- );
- if (mounted) {
- await Navigator.of(context).pushNamedAndRemoveUntil(
- IntroView.routeName,
- (_) => false,
- );
- }
- } else {
- await showFloatingFlushBar(
- type: FlushBarType.warning,
- message: "Something broke badly. Contact developer",
- context: context,
- );
-
- setState(() {
- _deleteInProgress = false;
- });
- }
- }
+ if (_deleteInProgress) return;
+ setState(() => _deleteInProgress = true);
+ await _deleteStack();
},
),
const SizedBox(height: 24),
### lib/route_generator.dart
@@ -1012,6 +1012,19 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
+ case SparkAddressOwnershipProofView.routeName:
+ if (args is ({String walletId, String address})) {
+ return getRoute(
+ shouldUseMaterialRoute: useMaterialPageRoute,
+ builder: (_) => SparkAddressOwnershipProofView(
+ walletId: args.walletId,
+ address: args.address,
+ ),
+ settings: RouteSettings(name: settings.name),
+ );
+ }
+ return _routeError("${settings.name} invalid args: ${args.toString()}");
+
case FusionProgressView.routeName:
if (args is String) {
return getRoute(
### lib/services/exchange/trocador/trocador_api.dart
@@ -31,18 +31,17 @@ const kTrocadorRefCode = "9eHm9BkQfS";
abstract class TrocadorAPI {
static const String authority = "api.trocador.app";
static const String onionAuthority =
- "trocadorfyhlu27aefre5u7zri66gudtzdyelymftvr4yjwcxhfaqsid.onion";
+ "65bsisadnxvw4kfz7h7a3jwcyenrhluuj3kd5toslfzxbk5q4m3wy6qd.onion";
static const String markup = "1";
static const String minKYCRating = "C";
static HTTP client = HTTP();
- static Uri _buildUri({
- required String method,
- required bool isOnion,
- Map<String, String>? params,
- }) {
- return isOnion
+ static bool get _useTor =>
+ AppConfig.hasFeature(AppFeature.tor) && Prefs.instance.useTor;
+
+ static Uri _buildUri({required String method, Map<String, String>? params}) {
+ return _useTor
? Uri.http(onionAuthority, method, params)
: Uri.https(authority, method, params);
}
@@ -57,11 +56,7 @@ abstract class TrocadorAPI {
"Content-Type": "application/json",
"API-KEY": kTrocadorApiKey,
},
- proxyInfo: !AppConfig.hasFeature(AppFeature.tor)
- ? null
- : Prefs.instance.useTor
- ? TorService.sharedInstance.getProxyInfo()
- : null,
+ proxyInfo: _useTor ? TorService.sharedInstance.getProxyInfo() : null,
);
code = response.code;
@@ -87,14 +82,8 @@ abstract class TrocadorAPI {
}
/// fetch all supported coins
- static Future<ExchangeResponse<List<TrocadorCoin>>> getCoins({
- required bool isOnion,
- }) async {
- final uri = _buildUri(
- isOnion: isOnion,
- method: "coins",
- params: {"ref": kTrocadorRefCode},
- );
+ static Future<ExchangeResponse<List<TrocadorCoin>>> getCoins() async {
+ final uri = _buildUri(method: "coins", params: {"ref": kTrocadorRefCode});
try {
final json = await _makeGetRequest(uri);
@@ -122,11 +111,9 @@ abstract class TrocadorAPI {
/// get trade info
static Future<ExchangeResponse<TrocadorTrade>> getTrade({
- required bool isOnion,
required String tradeId,
}) async {
final uri = _buildUri(
- isOnion: isOnion,
method: "trade",
params: {"ref": kTrocadorRefCode, "id": tradeId},
);
@@ -149,7 +136,6 @@ abstract class TrocadorAPI {
/// get standard/floating rate
static Future<ExchangeResponse<TrocadorRate>> getNewStandardRate({
- required bool isOnion,
required String fromTicker,
required String fromNetwork,
required String toTicker,
@@ -168,12 +154,11 @@ abstract class TrocadorAPI {
"markup": markup,
};
- return await _getNewRate(isOnion: isOnion, params: params);
+ return await _getNewRate(params: params);
}
/// get fixed rate/payment rate
static Future<ExchangeResponse<TrocadorRate>> getNewPaymentRate({
- required bool isOnion,
required String fromTicker,
required String fromNetwork,
required String toTicker,
@@ -192,14 +177,13 @@ abstract class TrocadorAPI {
"markup": markup,
};
- return await _getNewRate(isOnion: isOnion, params: params);
+ return await _getNewRate(params: params);
}
static Future<ExchangeResponse<TrocadorRate>> _getNewRate({
- required bool isOnion,
required Map<String, String> params,
}) async {
- final uri = _buildUri(isOnion: isOnion, method: "new_rate", params: params);
+ final uri = _buildUri(method: "new_rate", params: params);
try {
final json = await _makeGetRequest(uri);
@@ -219,7 +203,6 @@ abstract class TrocadorAPI {
/// create new floating rate/standard trade
static Future<ExchangeResponse<TrocadorTradeNew>> createNewStandardRateTrade({
- required bool isOnion,
required String? rateId,
required String fromTicker,
required String fromNetwork,
@@ -255,11 +238,10 @@ abstract class TrocadorAPI {
params["id"] = rateId;
}
- return await _getNewTrade(isOnion: isOnion, params: params);
+ return await _getNewTrade(params: params);
}
static Future<ExchangeResponse<TrocadorTradeNew>> createNewPaymentRateTrade({
- required bool isOnion,
required String? rateId,
required String fromTicker,
required String fromNetwork,
@@ -295,18 +277,13 @@ abstract class TrocadorAPI {
params["id"] = rateId;
}
- return await _getNewTrade(isOnion: isOnion, params: params);
+ return await _getNewTrade(params: params);
}
static Future<ExchangeResponse<TrocadorTradeNew>> _getNewTrade({
- required bool isOnion,
required Map<String, String> params,
}) async {
- final uri = _buildUri(
- isOnion: isOnion,
- method: "new_trade",
- params: params,
- );
+ final uri = _buildUri(method: "new_trade", params: params);
try {
final json = await _makeGetRequest(uri);
### lib/services/exchange/trocador/trocador_exchange.dart
@@ -69,7 +69,6 @@ class TrocadorExchange extends Exchange {
}) async {
final response = reversed
? await TrocadorAPI.createNewPaymentRateTrade(
- isOnion: false,
rateId: estimate?.rateId,
fromTicker: from.toLowerCase(),
fromNetwork: onlySupportedNetwork,
@@ -84,7 +83,6 @@ class TrocadorExchange extends Exchange {
isFixedRate: fixedRate,
)
: await TrocadorAPI.createNewStandardRateTrade(
- isOnion: false,
rateId: estimate?.rateId,
fromTicker: from.toLowerCase(),
fromNetwork: onlySupportedNetwork,
@@ -139,7 +137,7 @@ class TrocadorExchange extends Exchange {
Future<ExchangeResponse<List<Currency>>> getAllCurrencies(
bool fixedRate,
) async {
- _cachedCurrencies ??= (await TrocadorAPI.getCoins(isOnion: false)).value;
+ _cachedCurrencies ??= (await TrocadorAPI.getCoins()).value;
_cachedCurrencies?.removeWhere((e) => e.network != onlySupportedNetwork);
@@ -222,15 +220,13 @@ class TrocadorExchange extends Exchange {
) async {
final response = reversed
? await TrocadorAPI.getNewPaymentRate(
- isOnion: false,
fromTicker: from,
fromNetwork: onlySupportedNetwork,
toTicker: to,
toNetwork: onlySupportedNetwork,
toAmount: amount.toString(),
)
: await TrocadorAPI.getNewStandardRate(
- isOnion: false,
fromTicker: from,
fromNetwork: onlySupportedNetwork,
toTicker: to,
@@ -367,10 +363,7 @@ class TrocadorExchange extends Exchange {
@override
Future<ExchangeResponse<Trade>> updateTrade(Trade trade) async {
- final response = await TrocadorAPI.getTrade(
- isOnion: false,
- tradeId: trade.tradeId,
- );
+ final response = await TrocadorAPI.getTrade(tradeId: trade.tradeId);
if (response.value != null) {
final updated = response.value!;
### lib/themes/stack_theme_data.dart
@@ -0,0 +1,79 @@
+import 'package:flutter/material.dart';
+import 'package:google_fonts/google_fonts.dart';
+
+import '../utilities/constants.dart';
+import '../utilities/util.dart';
+import 'stack_colors.dart';
+
+ThemeData stackThemeData(StackColors colors) {
+ InputBorder outlineInputBorder() => OutlineInputBorder(
+ borderSide: BorderSide(width: 1, color: colors.textFieldDefaultBG),
+ borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius),
+ );
+
+ return ThemeData(
+ extensions: [colors],
+ highlightColor: colors.highlight,
+ brightness: colors.brightness,
+ fontFamily: GoogleFonts.inter().fontFamily,
+ unselectedWidgetColor: colors.radioButtonBorderDisabled,
+ radioTheme: const RadioThemeData(
+ splashRadius: 0,
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ ),
+ splashColor: Colors.transparent,
+ buttonTheme: ButtonThemeData(splashColor: colors.splash),
+ textButtonTheme: TextButtonThemeData(
+ style: ButtonStyle(
+ overlayColor: MaterialStateProperty.all(colors.splash),
+ minimumSize: MaterialStateProperty.all<Size>(const Size(46, 46)),
+ foregroundColor: MaterialStateProperty.all(colors.buttonTextSecondary),
+ backgroundColor: MaterialStateProperty.all<Color>(
+ colors.buttonBackSecondary,
+ ),
+ shape: MaterialStateProperty.all<OutlinedBorder>(
+ RoundedRectangleBorder(borderRadius: BorderRadius.circular(1000)),
+ ),
+ ),
+ ),
+ primaryColor: colors.accentColorDark,
+ primarySwatch: Util.createMaterialColor(colors.accentColorDark),
+ checkboxTheme: CheckboxThemeData(
+ splashRadius: 0,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(
+ Constants.size.checkboxBorderRadius,
+ ),
+ ),
+ checkColor: MaterialStateColor.resolveWith((state) {
+ if (state.contains(MaterialState.selected)) {
+ return colors.checkboxIconChecked;
+ }
+ return colors.checkboxBGChecked;
+ }),
+ fillColor: MaterialStateColor.resolveWith((states) {
+ if (states.contains(MaterialState.selected)) {
+ return colors.checkboxBGChecked;
+ }
+ return colors.checkboxBorderEmpty;
+ }),
+ ),
+ appBarTheme: AppBarTheme(
+ centerTitle: false,
+ color: colors.background,
+ surfaceTintColor: colors.background,
+ elevation: 0,
+ ),
+ inputDecorationTheme: InputDecorationTheme(
+ focusColor: colors.textFieldDefaultBG,
+ fillColor: colors.textFieldDefaultBG,
+ filled: true,
+ contentPadding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
+ enabledBorder: outlineInputBorder(),
+ focusedBorder: outlineInputBorder(),
+ errorBorder: outlineInputBorder(),
+ disabledBorder: outlineInputBorder(),
+ focusedErrorBorder: outlineInputBorder(),
+ ),
+ );
+}
### lib/utilities/desktop_startup_and_reset.dart
@@ -0,0 +1,394 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:archive/archive_io.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:isar_community/isar.dart';
+import 'package:path/path.dart' as path;
+
+import '../app_config.dart';
+import '../db/hive/db.dart';
+import '../db/isar/main_db.dart';
+import '../db/sqlite/firo_cache.dart';
+import '../models/isar/stack_theme.dart';
+import '../pages/already_running_view.dart';
+import '../themes/stack_colors.dart';
+import '../themes/stack_theme_data.dart';
+import '../themes/theme_providers.dart';
+import '../themes/theme_service.dart';
+import '../widgets/desktop/primary_button.dart';
+import '../widgets/desktop/secondary_button.dart';
+import 'stack_file_system.dart';
+import 'text_styles.dart';
+
+abstract final class DesktopStartupAndReset {
+ static const _dataDirectories = <String>[
+ 'isar',
+ 'wallets',
+ 'epiccash',
+ 'mimblewimblecoin',
+ 'xelis',
+ 'drift',
+ 'sqlite',
+ 'mwebd',
+ 'themes',
+ 'hive',
+ ];
+
+ // Keep this file and its handle until exit, including while deleting Hive.
+ static RandomAccessFile? _instanceLock;
+ static final Map<String, Future<_BundledAppearance>> _bundledAppearances = {};
+ static StackTheme? _displayTheme;
+
+ /// Run before opening Hive, Isar, or caches on desktop.
+ static Future<bool> prepareForStartup() async {
+ try {
+ await _acquireInstanceLock();
+ } on FileSystemException catch (e) {
+ if (isLockConflict(e)) {
+ await showAlreadyRunning();
+ return false;
+ }
+ rethrow;
+ }
+
+ if (!await isPending()) return true;
+ await askToFinishReset();
+ await showMessage('Resetting ${AppConfig.appName}...');
+ try {
+ await deleteData();
+ await clearPending();
+ } catch (e, s) {
+ debugPrint('Desktop reset failed: $e\n$s');
+ await showMessage(
+ 'Could not delete all ${AppConfig.appName} data.\n'
+ 'Close any program using it and reopen the app.',
+ );
+ return false;
+ }
+ return true;
+ }
+
+ static bool isLockConflict(FileSystemException e) =>
+ e.osError?.errorCode == 11 || e.message.contains('lock failed');
+
+ static Future<void> _acquireInstanceLock() async {
+ final root = await StackFileSystem.applicationRootDirectory();
+ _instanceLock = await File(path.join(root.path, '.instance.lock'))
+ .open(mode: FileMode.append);
+ await _instanceLock!.lock();
+ }
+
+ static Future<File> _marker() async => File(
+ path.join(
+ (await StackFileSystem.applicationRootDirectory()).path,
+ '.reset-pending',
+ ),
+ );
+
+ /// Never infer from password files that an unfinished reset was cancelled.
+ static Future<bool> isPending() async {
+ return (await _marker()).exists();
+ }
+
+ static Future<void> clearPending() async {
+ final marker = await _marker();
+ if (await marker.exists()) await marker.delete();
+ }
+
+ // Only called from the login reset flow, before wallets are loaded.
+ // The caller must exit afterwards, even if closing or deleting fails.
+ static Future<void> resetBeforeExit() async {
+ // Stays behind if the reset does not finish, so the next launch can ask.
+ await (await _marker()).writeAsString('reset', flush: true);
+
+ await _deleteKeys();
+ await FiroCacheCoordinator.close();
+ await DB.instance.hive.close();
+ for (final name in Isar.instanceNames.toList()) {
+ final isar = Isar.getInstance(name);
+ if (isar != null && !await isar.close()) {
+ throw StateError('Could not close Isar database $name');
+ }
+ }
+ await deleteData();
+ await clearPending();
+ }
+
+ static Future<void> deleteData() async {
+ final root = await StackFileSystem.applicationRootDirectory();
+ // Only app-owned paths: the root may be a custom -d directory or the
+ // iPad Library directory. Leave backups, exports, logs and the lock alone.
+ // tor/ is kept: it only holds Tor network state, and a running Tor client
+ // keeps files there open.
+ await _deleteKeys();
+ for (final name in _dataDirectories) {
+ await _deleteExceptSwb(Directory(path.join(root.path, name)));
+ }
+ }
+
+ static Future<void> _deleteExceptSwb(FileSystemEntity entity) async {
+ final type = await FileSystemEntity.type(entity.path, followLinks: false);
+ if (type == FileSystemEntityType.notFound) return;
+
+ if (type == FileSystemEntityType.directory) {
+ final directory = Directory(entity.path);
+ await for (final child in directory.list(followLinks: false)) {
+ await _deleteExceptSwb(child);
+ }
+ if (await directory.list(followLinks: false).isEmpty) {
+ await directory.delete();
+ }
+ } else if (!entity.path.toLowerCase().endsWith('.swb')) {
+ // Delete links themselves, never the files or directories they target.
+ if (type == FileSystemEntityType.link) {
+ await Link(entity.path).delete();
+ } else {
+ await File(entity.path).delete();
+ }
+ }
+ }
+
+ // The password, then the secure storage holding the wallet keys. Deleted
+ // first, so a reset is permanent once it starts. Neither is open before
+ // login.
+ static Future<void> _deleteKeys() async {
+ final root = (await StackFileSystem.applicationRootDirectory()).path;
+ for (final file in [
+ // Hive restores .hivec if .hive is missing. Remove it first so a crash
+ // cannot resurrect the password after deleting .hive.
+ for (final ext in ['hivec', 'hive', 'lock'])
+ File(path.join(root, 'hive', 'desktopdata.$ext')),
+ for (final ext in ['isar', 'isar-lck', 'isar.lock'])
+ File(path.join(root, 'isar', 'desktopStore.$ext')),
+ ]) {
+ if (await file.exists()) await file.delete();
+ }
+ }
+
+ static Future<void> askToFinishReset() async {
+ final done = Completer<void>();
+ await _show(
+ title: 'Unfinished reset',
+ message:
+ 'A reset of ${AppConfig.appName} did not finish. Continuing will '
+ 'delete current wallet data and settings, including anything '
+ 'created since the reset began. Files ending in .swb will be kept.',
+ primaryAction: (
+ label: 'Retry deleting',
+ onPressed: () {
+ if (!done.isCompleted) done.complete();
+ },
+ ),
+ secondaryAction: (label: 'Quit', onPressed: () => exit(0)),
+ );
+ return done.future;
+ }
+
+ static Future<void> showMessage(String message, {StackTheme? theme}) =>
+ _show(title: AppConfig.appName, message: message, theme: theme);
+
+ static Future<void> showAlreadyRunning() async {
+ try {
+ await StackFileSystem.initThemesDir();
+ await MainDB.instance.initMainDB();
+ ThemeService.instance.init(MainDB.instance);
+ final theme = ThemeService.instance.getTheme(themeId: 'light');
+ if (theme == null) throw StateError('Default theme is unavailable');
+ _showScreen(const AlreadyRunningView(), theme);
+ } catch (_) {
+ await _show(
+ title: AppConfig.appName,
+ message: 'is already running.\nClose the other window and try again.',
+ );
+ }
+ }
+
+ static Future<void> _show({
+ required String title,
+ required String message,
+ ({String label, VoidCallback onPressed})? primaryAction,
+ ({String label, VoidCallback onPressed})? secondaryAction,
+ StackTheme? theme,
+ }) async {
+ final screenTheme =
+ theme ?? _displayTheme ?? (await _bundledAppearance('light')).theme;
+ final logoTheme =
+ screenTheme.brightness == Brightness.dark &&
+ AppConfig.hasFeature(AppFeature.themeSelection)
+ ? 'dark'
+ : 'light';
+ final appearance = await _bundledAppearance(logoTheme);
+ _showScreen(
+ _DesktopStatusView(
+ title: title,
+ message: message,
+ logo: appearance.logo,
+ logoIsPng: appearance.logoIsPng,
+ primaryAction: primaryAction,
+ secondaryAction: secondaryAction,
+ ),
+ screenTheme,
+ );
+ }
+
+ static Future<_BundledAppearance> _bundledAppearance(String name) =>
+ _bundledAppearances.putIfAbsent(name, () => _loadBundledAppearance(name));
+
+ static Future<_BundledAppearance> _loadBundledAppearance(String name) async {
+ final data = await rootBundle.load('assets/default_themes/$name.zip');
+ final archive = ZipDecoder().decodeBytes(data.buffer.asUint8List());
+ final themeJson = archive.files.singleWhere(
+ (file) => file.name == 'theme.json',
+ );
+ final json = Map<String, dynamic>.from(
+ jsonDecode(utf8.decode(themeJson.content as List<int>)) as Map,
+ );
+ final logoPath =
+ 'assets/${(json['assets'] as Map)['stack_icon'] as String}';
+ final logo = archive.files.singleWhere((file) => file.name == logoPath);
+ return _BundledAppearance(
+ theme: StackTheme.fromJson(json: json),
+ logo: Uint8List.fromList(logo.content as List<int>),
+ logoIsPng: logoPath.toLowerCase().endsWith('.png'),
+ );
+ }
+
+ static void _showScreen(Widget home, StackTheme theme) {
+ _displayTheme = theme;
+ runApp(
+ KeyedSubtree(
+ // Dispose the normal ProviderScope before its databases are closed.
+ key: UniqueKey(),
+ child: ProviderScope(
+ overrides: [
+ themeProvider.overrideWithProvider(
+ StateProvider<StackTheme>((ref) => theme),
+ ),
+ ],
+ child: _DesktopAppShell(home: home),
+ ),
+ ),
+ );
+ }
+}
+
+class _DesktopAppShell extends ConsumerWidget {
+ const _DesktopAppShell({required this.home});
+
+ final Widget home;
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final colorScheme = ref.watch(colorProvider.state).state;
+ return MaterialApp(
+ title: AppConfig.appName,
+ theme: stackThemeData(colorScheme),
+ home: home,
+ );
+ }
+}
+
+class _BundledAppearance {
+ const _BundledAppearance({
+ required this.theme,
+ required this.logo,
+ required this.logoIsPng,
+ });
+
+ final StackTheme theme;
+ final Uint8List logo;
+ final bool logoIsPng;
+}
+
+class _DesktopStatusView extends StatelessWidget {
+ const _DesktopStatusView({
+ required this.title,
+ required this.message,
+ required this.logo,
+ required this.logoIsPng,
+ this.primaryAction,
+ this.secondaryAction,
+ });
+
+ final String title;
+ final String message;
+ final Uint8List logo;
+ final bool logoIsPng;
+ final ({String label, VoidCallback onPressed})? primaryAction;
+ final ({String label, VoidCallback onPressed})? secondaryAction;
+
+ @override
+ Widget build(BuildContext context) {
+ final colors = Theme.of(context).extension<StackColors>()!;
+ return Material(
+ color: colors.background,
+ child: DecoratedBox(
+ decoration: BoxDecoration(
+ color: colors.background,
+ gradient: colors.gradientBackground,
+ ),
+ child: SafeArea(
+ child: LayoutBuilder(
+ builder: (context, constraints) => SingleChildScrollView(
+ padding: const EdgeInsets.symmetric(horizontal: 32),
+ child: ConstrainedBox(
+ constraints: BoxConstraints(minHeight: constraints.maxHeight),
+ child: Center(
+ child: SizedBox(
+ width: 480,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ SizedBox(
+ width: 100,
+ height: 100,
+ child: logoIsPng
+ ? Image.memory(logo, fit: BoxFit.contain)
+ : SvgPicture.memory(logo, fit: BoxFit.contain),
+ ),
+ const SizedBox(height: 42),
+ Text(
+ title,
+ textAlign: TextAlign.center,
+ style: STextStyles.desktopH1(context),
+ ),
+ const SizedBox(height: 24),
+ Text(
+ message,
+ textAlign: TextAlign.center,
+ style: STextStyles.desktopTextSmall(context)
+ .copyWith(color: colors.textSubtitle1),
+ ),
+ if (primaryAction != null) ...[
+ const SizedBox(height: 48),
+ PrimaryButton(
+ label: primaryAction!.label,
+ onPressed: primaryAction!.onPressed,
+ ),
+ ],
+ if (secondaryAction != null) ...[
+ const SizedBox(height: 24),
+ SecondaryButton(
+ label: secondaryAction!.label,
+ onPressed: secondaryAction!.onPressed,
+ ),
+ ],
+ if (primaryAction != null || secondaryAction != null)
+ const SizedBox(height: 96),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
### lib/utilities/util.dart
@@ -16,6 +16,7 @@ import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:intl/number_symbols.dart';
import 'package:intl/number_symbols_data.dart';
+import 'package:platform/platform.dart' as platform;
import '../app_config.dart';
import '../wallets/wallet/impl/monero_wallet.dart';
@@ -30,6 +31,7 @@ abstract class Util {
static final Map<String, NumberSymbols?> _numberSymbolsCache = {};
+ static platform.Platform layoutPlatform = const platform.LocalPlatform();
static double? screenWidth;
static bool? isIpad;
@@ -84,16 +86,17 @@ abstract class Util {
}
static bool get isDesktop {
+ final p = layoutPlatform;
// special check for running on linux based phones
- if (Platform.isLinux && screenWidth != null && screenWidth! < 800) {
+ if (p.isLinux && screenWidth != null && screenWidth! < 800) {
return false;
}
if (isIpad == true) {
return true;
}
- return Platform.isLinux || Platform.isMacOS || Platform.isWindows;
+ return p.isLinux || p.isMacOS || p.isWindows;
}
static Future<bool> get getIsIPad async {
### lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart
@@ -130,6 +130,43 @@ bool shouldSubtractSparkFeeFromAmount({
void initSparkLogging(Level level) => libSpark.initSparkLogging(level);
+({String? proof, String? error}) _createSparkAddressOwnershipProof(
+ ({String message, String privateKeyHex, int spendKeyIndex, int diversifier})
+ args,
+) {
+ try {
+ return (
+ proof: libSpark.createSparkAddressOwnershipProof(
+ message: args.message,
+ privateKeyHex: args.privateKeyHex,
+ spendKeyIndex: args.spendKeyIndex,
+ diversifier: args.diversifier,
+ ),
+ error: null,
+ );
+ } catch (e) {
+ return (proof: null, error: e.toString());
+ }
+}
+
+({bool? valid, String? error}) _verifySparkAddressOwnershipProof(
+ ({String message, String address, String proof, bool isTestNet}) args,
+) {
+ try {
+ return (
+ valid: libSpark.verifySparkAddressOwnershipProof(
+ message: args.message,
+ address: args.address,
+ proof: args.proof,
+ isTestNet: args.isTestNet,
+ ),
+ error: null,
+ );
+ } catch (e) {
+ return (valid: null, error: e.toString());
+ }
+}
+
abstract class _SparkIsolate {
static Isolate? _isolate;
static SendPort? _sendPort;
@@ -189,6 +226,8 @@ Future<R> computeWithLibSparkLogging<M, R>(
mixin SparkInterface<T extends ElectrumXCurrencyInterface>
on Bip39HDWallet<T>, ElectrumXInterface<T> {
+ static const _sparkNameLookAheadCount = 100;
+
Address? _currentSparkAddress;
String? _viewKeyHex;
@@ -226,13 +265,15 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
);
}
- final sparkAddress =
- await computeWithLibSparkLogging(_getAddressFromFullViewKey, (
- fullViewKeyHex: _viewKeyHex!,
- index: sparkIndex,
- diversifier: diversifier,
- isTestNet: isTestNet,
- ));
+ final sparkAddress = await computeWithLibSparkLogging(
+ _getAddressFromFullViewKey,
+ (
+ fullViewKeyHex: _viewKeyHex!,
+ index: sparkIndex,
+ diversifier: diversifier,
+ isTestNet: isTestNet,
+ ),
+ );
return Address(
walletId: walletId,
@@ -1349,9 +1390,8 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
}
for (final transaction in transactions) {
if (transaction.usedSparkCoins!.length > 1) {
- final transactionVersion = btc.Transaction.fromHex(
- transaction.raw!,
- ).version;
+ final transactionVersion = btc.Transaction.fromHex(transaction.raw!)
+ .version;
if (!isChaumV2SparkTransactionVersion(transactionVersion)) {
throw Exception(
"Refusing to broadcast a multi-input Chaum V1 transaction.",
@@ -1872,12 +1912,10 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
// some look ahead
// TODO revisit this and clean up (track pre gen'd addresses instead of
// generating every time) arbitrary number of addresses
- const lookAheadCount = 100;
-
// force unwrap optional should be fine here. If not then the
// eclosing function is being called somewhere it probably shouldn't be.
int diversifier = _currentSparkAddress!.derivationIndex;
- final maxDiversifier = diversifier + lookAheadCount;
+ final maxDiversifier = diversifier + _sparkNameLookAheadCount;
while (diversifier < maxDiversifier) {
// change address check
@@ -2915,6 +2953,106 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
return txData;
}
+ @override
+ Future<String> signMessage(String message, {required Address address}) {
+ if (address.type == AddressType.spark) {
+ return createSparkAddressOwnershipProof(
+ address: address.value,
+ message: message,
+ );
+ }
+ return super.signMessage(message, address: address);
+ }
+
+ @override
+ Future<bool> verifyMessage(
+ String message, {
+ required String address,
+ required String signature,
+ }) async {
+ if (!validateSparkAddress(address: address, isTestNet: false) &&
+ !validateSparkAddress(address: address, isTestNet: true)) {
+ return super.verifyMessage(
+ message,
+ address: address,
+ signature: signature,
+ );
+ }
+ final result = await computeWithLibSparkLogging(
+ _verifySparkAddressOwnershipProof,
+ (
+ message: message,
+ address: address,
+ proof: signature,
+ isTestNet: isTestNet,
+ ),
+ );
+ if (result.error != null) throw Exception(result.error);
+ return result.valid!;
+ }
+
+ Future<String> createSparkAddressOwnershipProof({
+ required String address,
+ required String message,
+ }) async {
+ if (isViewOnly) {
+ throw Exception(
+ "Cannot create an ownership proof from a view only wallet",
+ );
+ }
+
+ if (message.trim().isEmpty) {
+ throw Exception("Message must not be blank");
+ }
+
+ Address? sparkAddress = await mainDB.getAddress(walletId, address);
+ if (sparkAddress == null) {
+ final currentDiversifier =
+ (await getCurrentReceivingSparkAddress())?.derivationIndex;
+ if (currentDiversifier != null) {
+ var diversifier = currentDiversifier;
+ final maxDiversifier = diversifier + _sparkNameLookAheadCount;
+ while (diversifier < maxDiversifier) {
+ if (diversifier == libSpark.sparkChange) {
+ diversifier++;
+ }
+ final candidate = await _generateSparkAddress(diversifier++);
+ if (candidate.value == address) {
+ sparkAddress = candidate;
+ break;
+ }
+ }
+ }
+ }
+ if (sparkAddress == null || sparkAddress.type != AddressType.spark) {
+ throw Exception("Spark address does not belong to this wallet");
+ }
+ if (sparkAddress.derivationIndex < 0) {
+ throw Exception("Spark address diversifier is unavailable");
+ }
+
+ final root = await getRootHDNode();
+ final privateKeyHex = root
+ .derivePath(sparkDerivationPath)
+ .privateKey
+ .data
+ .toHex;
+
+ final result = await computeWithLibSparkLogging(
+ _createSparkAddressOwnershipProof,
+ (
+ message: message,
+ privateKeyHex: privateKeyHex,
+ spendKeyIndex: sparkIndex,
+ diversifier: sparkAddress.derivationIndex,
+ ),
+ );
+ if (result.error != null) {
+ throw Exception(result.error);
+ }
+ return result.proof!;
+ }
+
@override
Future<void> updateBalance() async {
// call to super to update transparent balance
### lib/widgets/textfields/adaptive_text_field.dart
@@ -19,6 +19,8 @@ class AdaptiveTextField extends StatefulWidget {
this.focusNode,
this.style,
this.autocorrect,
+ this.smartDashesType,
+ this.smartQuotesType,
this.desktopMed = false,
this.readOnly = false,
this.enabled = true,
@@ -35,6 +37,7 @@ class AdaptiveTextField extends StatefulWidget {
this.maxLines,
this.inputFormatters,
this.showPasteClearButton = false,
+ this.trimPastedText = true,
this.keyboardType,
});
@@ -46,6 +49,8 @@ class AdaptiveTextField extends StatefulWidget {
final FocusNode? focusNode;
final TextStyle? style;
final bool? autocorrect;
+ final SmartDashesType? smartDashesType;
+ final SmartQuotesType? smartQuotesType;
final bool desktopMed;
final EdgeInsets? contentPadding;
final int? minLines;
@@ -63,6 +68,8 @@ class AdaptiveTextField extends StatefulWidget {
/// This will be ignored if [suffixIcons] is not null!
final bool showPasteClearButton;
+ final bool trimPastedText;
+
/// If this is not null, [showPasteClearButton] will be ignored.
final List<Widget>? suffixIcons;
@@ -148,6 +155,8 @@ class _AdaptiveTextFieldState extends State<AdaptiveTextField> {
readOnly: widget.readOnly,
enabled: widget.enabled,
autocorrect: widget.autocorrect,
+ smartDashesType: widget.smartDashesType,
+ smartQuotesType: widget.smartQuotesType,
enableSuggestions: widget.enableSuggestions,
onSubmitted: widget.onSubmitted,
keyboardType: widget.keyboardType,
@@ -201,7 +210,9 @@ class _AdaptiveTextFieldState extends State<AdaptiveTextField> {
await Clipboard.getData(Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
- final content = data.text!.trim();
+ final content = widget.trimPastedText
+ ? data.text!.trim()
+ : data.text!;
// Setting controller.text directly skips
// inputFormatters, so run them here as a
// paste into the (empty) field would.
### lib/wl_gen/interfaces/lib_spark_interface.dart
@@ -77,6 +77,20 @@ abstract class LibSparkInterface {
required bool ignoreProof,
});
+ String createSparkAddressOwnershipProof({
+ required String message,
+ required String privateKeyHex,
+ required int spendKeyIndex,
+ required int diversifier,
+ });
+
+ bool verifySparkAddressOwnershipProof({
+ required String message,
+ required String address,
+ required String proof,
+ required bool isTestNet,
+ });
+
Uint8List getSparkNameCommitment({
required Uint8List serializedSparkNameData,
});
### pubspec.lock
@@ -1023,8 +1023,8 @@ packages:
dependency: "direct main"
description:
path: "."
- ref: fe505c29f5dbb5dd94fb986e509506be9e2f37e7
- resolved-ref: fe505c29f5dbb5dd94fb986e509506be9e2f37e7
+ ref: "3fdcb21160a39c051a0e73d7ae04f987134ecd5f"
+ resolved-ref: "3fdcb21160a39c051a0e73d7ae04f987134ecd5f"
url: "https://github.com/cypherstack/flutter_libsparkmobile.git"
source: git
version: "0.2.0"
@@ -1823,7 +1823,7 @@ packages:
source: hosted
version: "2.2.1"
path_provider_platform_interface:
- dependency: transitive
+ dependency: "direct dev"
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
@@ -1903,7 +1903,7 @@ packages:
source: hosted
version: "0.6.0"
platform:
- dependency: transitive
+ dependency: "direct main"
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
@@ -2135,10 +2135,10 @@ packages:
dependency: "direct main"
description:
name: socks5_proxy
- sha256: e0cba6917cd374de6f6cb0ce081e50e6efc24c61644b8e9f20c8bf8b91bb0b75
+ sha256: "80fa31a9ebfc0dc8de7b0e568c8d8927b65558ef2c7591cbee5afac814fb8f74"
url: "https://pub.dev"
source: hosted
- version: "1.0.3+dev.3"
+ version: "2.1.1"
socks_socket:
dependency: transitive
description:
### scripts/app_config/templates/pubspec.template.yaml
@@ -21,6 +21,7 @@ dependencies:
flutter:
sdk: flutter
ffi: ^2.0.1
+ platform: ^3.1.6
mutex: ^3.0.0
web_socket_channel: ^2.4.0
@@ -46,7 +47,7 @@ dependencies:
# flutter_libsparkmobile:
# git:
# url: https://github.com/cypherstack/flutter_libsparkmobile.git
-# ref: fe505c29f5dbb5dd94fb986e509506be9e2f37e7
+# ref: 3fdcb21160a39c051a0e73d7ae04f987134ecd5f
# %%END_ENABLE_FIRO%%
# %%ENABLE_EPIC%%
@@ -216,7 +217,7 @@ dependencies:
git:
url: https://github.com/cypherstack/tezart.git
ref: 84c563104f1a19c26e49bafccb7da404b210b666
- socks5_proxy: 1.0.3+dev.3
+ socks5_proxy: ^2.1.1
convert: ^3.1.1
flutter_hooks: ^0.20.3
meta: ^1.9.1
@@ -282,6 +283,7 @@ dev_dependencies:
sdk: flutter
integration_test:
sdk: flutter
+ path_provider_platform_interface: ^2.1.2
build_runner: ^2.15.1
flutter_launcher_icons: ^0.13.1
hive_ce_generator: ^1.11.1
### scripts/test_desktop_forgot_password.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Temporary workaround for https://github.com/flutter/flutter/issues/135673.
+# Remove this script when Flutter is updated to 3.49 or later. Then run:
+# flutter test integration_test/desktop_forgot_password -d macos
+# Usage: bash scripts/test_desktop_forgot_password.sh [device] [test options]
+# Set FLUTTER_BIN to use a Flutter executable other than the one on PATH.
+
+cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
+reset_test_flutter="${FLUTTER_BIN:-flutter}"
+reset_test_device="${1:-macos}"
+if (( $# > 0 )); then shift; fi
+
+reset_test_status=0
+for reset_test_file in integration_test/desktop_forgot_password/*_test.dart; do
+ if [[ ! -f "$reset_test_file" ]]; then
+ printf 'No desktop forgot-password test files found.\n' >&2
+ exit 2
+ fi
+ if "$reset_test_flutter" test -d "$reset_test_device" "$reset_test_file" "$@"; then
+ continue
+ else
+ reset_test_exit=$?
+ case "$reset_test_exit" in
+ 130|143) exit "$reset_test_exit" ;;
+ esac
+ reset_test_status=1
+ fi
+done
+exit "$reset_test_status"
### test/networking/http_socks_proxy_test.dart
@@ -0,0 +1,82 @@
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:stackwallet/networking/http.dart';
+
+// Minimal SOCKS5 server that records the CONNECT target and answers any
+// request with a canned HTTP 200.
+class _FakeSocksServer {
+ late final ServerSocket _server;
+ int? addressType;
+ String? target;
+ int? port;
+
+ int get listeningPort => _server.port;
+
+ Future<void> start() async {
+ _server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
+ _server.listen((socket) {
+ var stage = 0;
+ socket.listen((Uint8List bytes) {
+ switch (stage) {
+ case 0:
+ socket.add([0x05, 0x00]);
+ stage = 1;
+ case 1:
+ addressType = bytes[3];
+ if (addressType == 0x03) {
+ target = String.fromCharCodes(bytes.sublist(5, 5 + bytes[4]));
+ } else {
+ target = bytes.sublist(4, bytes.length - 2).join('.');
+ }
+ port = (bytes[bytes.length - 2] << 8) | bytes[bytes.length - 1];
+ socket.add([0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]);
+ stage = 2;
+ default:
+ socket.write(
+ 'HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok',
+ );
+ socket.close();
+ }
+ });
+ });
+ }
+
+ Future<void> stop() => _server.close();
+}
+
+void main() {
+ late _FakeSocksServer socks;
+
+ setUp(() async {
+ socks = _FakeSocksServer();
+ await socks.start();
+ });
+
+ tearDown(() => socks.stop());
+
+ Future<Response> get(String host) => const HTTP().get(
+ url: Uri.http(host, '/'),
+ proxyInfo: (host: InternetAddress.loopbackIPv4, port: socks.listeningPort),
+ );
+
+ test('proxied request sends the hostname to the SOCKS5 proxy', () async {
+ final response = await get('example.invalid');
+
+ expect(response.code, 200);
+ expect(socks.addressType, 0x03);
+ expect(socks.target, 'example.invalid');
+ expect(socks.port, 80);
+ });
+
+ test('proxied request can target an onion address', () async {
+ const onion =
+ 'trocadorfyhlu27aefre5u7zri66gudtzdyelymftvr4yjwcxhfaqsid.onion';
+ final response = await get(onion);
+
+ expect(response.code, 200);
+ expect(socks.addressType, 0x03);
+ expect(socks.target, onion);
+ });
+}
### test/pages/spark_address_ownership_proof_test.dart
@@ -0,0 +1,314 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+import 'package:platform/platform.dart' as platform;
+import 'package:stackwallet/db/drift/database.dart';
+import 'package:stackwallet/db/isar/main_db.dart';
+import 'package:stackwallet/models/isar/models/isar_models.dart';
+import 'package:stackwallet/models/isar/stack_theme.dart';
+import 'package:stackwallet/pages/signing/signing_view.dart';
+import 'package:stackwallet/pages/signing/sub_widgets/sign_message_tab.dart';
+import 'package:stackwallet/pages/signing/sub_widgets/verify_message_tab.dart';
+import 'package:stackwallet/pages/spark_names/sub_widgets/spark_name_details.dart';
+import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'
+ show IconCopyButton;
+import 'package:stackwallet/providers/global/wallets_provider.dart';
+import 'package:stackwallet/providers/db/drift_provider.dart';
+import 'package:stackwallet/providers/db/main_db_provider.dart';
+import 'package:stackwallet/route_generator.dart';
+import 'package:stackwallet/services/wallets.dart';
+import 'package:stackwallet/themes/stack_colors.dart';
+import 'package:stackwallet/themes/theme_providers.dart';
+import 'package:stackwallet/utilities/util.dart';
+import 'package:stackwallet/utilities/stack_file_system.dart';
+import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
+import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
+import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart';
+import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
+import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
+import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
+import 'package:stackwallet/widgets/desktop/primary_button.dart';
+import 'package:stackwallet/widgets/dialogs/s_dialog.dart';
+
+import '../sample_data/theme_json.dart';
+
+class _ProofWallet extends FiroWallet {
+ _ProofWallet() : super(CryptoCurrencyNetwork.main);
+ String? signedMessage;
+ bool viewOnly = false;
+ @override
+ bool get isViewOnly => viewOnly;
+ @override
+ Future<String> createSparkAddressOwnershipProof({
+ required String address,
+ required String message,
+ }) async {
+ signedMessage = message;
+ return 'ab' * 130;
+ }
+}
+
+class _Wallets extends Mock implements Wallets {
+ final wallet = _ProofWallet();
+ @override
+ FiroWallet getWallet(String walletId) => wallet;
+}
+
+class _LabelDB extends Mock implements MainDB {
+ @override
+ AddressLabel? getAddressLabelSync(String walletId, String address) => null;
+}
+
+void main() {
+ setUp(() {
+ final previousPlatform = Util.layoutPlatform;
+ final previousWidth = Util.screenWidth;
+ final previousIsIpad = Util.isIpad;
+ addTearDown(() {
+ Util.layoutPlatform = previousPlatform;
+ Util.screenWidth = previousWidth;
+ Util.isIpad = previousIsIpad;
+ });
+ Util.layoutPlatform = platform.FakePlatform(operatingSystem: 'android');
+ Util.screenWidth = null;
+ Util.isIpad = false;
+ });
+
+ setUpAll(() async {
+ final directory = await Directory.systemTemp.createTemp('spark-proof-ui-');
+ StackFileSystem.setDesktopOverrideDir(directory.path);
+ addTearDown(() => directory.delete(recursive: true));
+ const channel = MethodChannel('plugins.flutter.io/path_provider');
+ final messenger =
+ TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
+ messenger.setMockMethodCallHandler(channel, (_) async => directory.path);
+ addTearDown(() => messenger.setMockMethodCallHandler(channel, null));
+ final font = FontLoader('Inter_500');
+ font.addFont(rootBundle.load('google_fonts/Inter-Medium.ttf'));
+ await font.load();
+ });
+
+ for (final desktop in [false, true]) {
+ testWidgets(
+ '${desktop ? "desktop" : "mobile"} proof layout and exact message',
+ (tester) async {
+ Util.layoutPlatform = platform.FakePlatform(
+ operatingSystem: desktop ? 'macos' : 'android',
+ );
+ Util.screenWidth = desktop ? 1000 : 390;
+ await tester.binding.setSurfaceSize(
+ Size(desktop ? 1000 : 390, desktop ? 600 : 844),
+ );
+ addTearDown(() => tester.binding.setSurfaceSize(null));
+ final wallets = _Wallets();
+ final view = SparkAddressOwnershipProofView(
+ walletId: 'test',
+ address: 'sm1${'a' * 141}',
+ );
+ await tester.pumpWidget(
+ ProviderScope(
+ overrides: [
+ pWallets.overrideWithValue(wallets),
+ themeProvider.overrideWithValue(
+ StateController(StackTheme.fromJson(json: lightThemeJsonMap)),
+ ),
+ ],
+ child: MaterialApp(
+ theme: ThemeData(
+ extensions: [
+ StackColors.fromStackColorTheme(
+ StackTheme.fromJson(json: lightThemeJsonMap),
+ ),
+ ],
+ ),
+ onGenerateRoute: RouteGenerator.generateRoute,
+ home: desktop
+ ? SDialog(child: view)
+ : Builder(
+ builder: (context) => Scaffold(
+ body: TextButton(
+ onPressed: () => Navigator.of(context).pushNamed(
+ SparkAddressOwnershipProofView.routeName,
+ arguments: (
+ walletId: view.walletId,
+ address: view.address,
+ ),
+ ),
+ child: const Text('Open proof'),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ if (!desktop) {
+ await tester.tap(find.text('Open proof'));
+ await tester.pumpAndSettle();
+ }
+ expect(find.byType(AppBar), desktop ? findsNothing : findsOneWidget);
+ expect(
+ find.byType(DesktopDialogCloseButton),
+ desktop ? findsOneWidget : findsNothing,
+ );
+ final field = find.byType(TextField);
+ expect(
+ tester.widget<TextField>(field).smartDashesType,
+ SmartDashesType.disabled,
+ );
+ expect(
+ tester.widget<TextField>(field).smartQuotesType,
+ SmartQuotesType.disabled,
+ );
+ await tester.enterText(field, ' \n\t');
+ await tester.pump();
+ expect(
+ tester.widget<PrimaryButton>(find.byType(PrimaryButton)).enabled,
+ isFalse,
+ );
+ const message = ' challenge\n ';
+ await tester.enterText(field, message);
+ await tester.pump();
+ await tester.ensureVisible(find.text('Create proof'));
+ await tester.tap(find.text('Create proof'));
+ await tester.pumpAndSettle();
+ expect(wallets.wallet.signedMessage, message);
+ expect(
+ find.byType(IconCopyButton),
+ desktop ? findsOneWidget : findsNothing,
+ );
+ expect(
+ find.byType(SimpleCopyButton),
+ desktop ? findsNothing : findsOneWidget,
+ );
+ await tester.enterText(field, 'changed');
+ await tester.pump();
+ expect(find.text('ab' * 130), findsNothing);
+ expect(tester.takeException(), isNull);
+ if (!desktop) {
+ await tester.tap(find.byType(AppBarBackButton));
+ await tester.pumpAndSettle();
+ expect(find.text('Open proof'), findsOneWidget);
+ expect(find.byType(SparkAddressOwnershipProofView), findsNothing);
+ }
+ },
+ );
+ }
+
+ for (final ipad in [false, true]) {
+ testWidgets('ownership action layout on ${ipad ? "iPad" : "small phone"}', (
+ tester,
+ ) async {
+ Util.layoutPlatform = platform.FakePlatform(operatingSystem: 'ios');
+ Util.screenWidth = ipad ? 1024 : 320;
+ Util.isIpad = ipad;
+ final size = Size(ipad ? 1024 : 320, ipad ? 768 : 568);
+ await tester.binding.setSurfaceSize(size);
+ addTearDown(() => tester.binding.setSurfaceSize(null));
+ tester.view.devicePixelRatio = 1;
+ addTearDown(tester.view.resetDevicePixelRatio);
+ addTearDown(tester.view.resetViewInsets);
+ final walletId = 'proof-layout-$ipad';
+ final db = (await tester.runAsync(() async {
+ final db = Drift.get(walletId);
+ await db.customSelect('SELECT 1').get();
+ return db;
+ }))!;
+ addTearDown(db.close);
+ try {
+ final theme = StackTheme.fromJson(json: lightThemeJsonMap);
+ final view = SparkNameDetailsView(
+ walletId: walletId,
+ name: SparkName(
+ name: 'example',
+ address: 'sm1${'a' * 141}',
+ validUntil: 10000,
+ ),
+ );
+ await tester.pumpWidget(
+ ProviderScope(
+ overrides: [
+ pWallets.overrideWithValue(_Wallets()),
+ mainDBProvider.overrideWithValue(_LabelDB()),
+ pDrift(walletId).overrideWithValue(db),
+ pWalletChainHeight(walletId).overrideWithValue(0),
+ themeProvider.overrideWithValue(StateController(theme)),
+ ],
+ child: MaterialApp(
+ theme: ThemeData(
+ extensions: [StackColors.fromStackColorTheme(theme)],
+ textButtonTheme: TextButtonThemeData(
+ style: TextButton.styleFrom(minimumSize: const Size(46, 48)),
+ ),
+ ),
+ builder: (context, child) => MediaQuery(
+ data: MediaQuery.of(context).copyWith(
+ size: size,
+ textScaler: TextScaler.linear(ipad ? 1 : 1.5),
+ ),
+ child: child!,
+ ),
+ onGenerateRoute: RouteGenerator.generateRoute,
+ home: ipad ? SDialog(child: view) : view,
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+ final action = find.text('Prove address ownership');
+ await tester.ensureVisible(action);
+ expect(tester.takeException(), isNull);
+ await tester.tap(action);
+ await tester.pumpAndSettle();
+ if (ipad) {
+ tester.view.viewInsets = const FakeViewPadding(bottom: 350);
+ await tester.pumpAndSettle();
+ }
+ await tester.enterText(find.byType(TextField), 'challenge');
+ await tester.pumpAndSettle();
+ await tester.ensureVisible(find.text('Create proof'));
+ expect(
+ tester.getRect(find.text('Create proof')).bottom,
+ lessThanOrEqualTo(size.height - (ipad ? 350 : 0)),
+ );
+ expect(tester.takeException(), isNull);
+ } finally {
+ await tester.pumpWidget(const SizedBox.shrink());
+ await tester.pumpAndSettle();
+ }
+ });
+ }
+
+ testWidgets('view-only wallet exposes verification without signing', (
+ tester,
+ ) async {
+ final wallets = _Wallets();
+ wallets.wallet.viewOnly = true;
+ await tester.pumpWidget(
+ ProviderScope(
+ overrides: [
+ pWallets.overrideWithValue(wallets),
+ themeProvider.overrideWithValue(
+ StateController(StackTheme.fromJson(json: lightThemeJsonMap)),
+ ),
+ ],
+ child: MaterialApp(
+ theme: ThemeData(
+ extensions: [
+ StackColors.fromStackColorTheme(
+ StackTheme.fromJson(json: lightThemeJsonMap),
+ ),
+ ],
+ ),
+ home: const Scaffold(body: SigningView(walletId: 'test')),
+ ),
+ ),
+ );
+ expect(find.byType(VerifyMessageForm), findsOneWidget);
+ expect(find.byType(SignMessageForm), findsNothing);
+ expect(find.text('Sign message'), findsNothing);
+ expect(tester.takeException(), isNull);
+ });
+}
### test/utilities/util_layout_test.dart
@@ -0,0 +1,38 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:platform/platform.dart' as platform;
+import 'package:stackwallet/utilities/util.dart';
+
+void main() {
+ setUp(() {
+ final previousPlatform = Util.layoutPlatform;
+ final previousWidth = Util.screenWidth;
+ final previousIsIpad = Util.isIpad;
+ addTearDown(() {
+ Util.layoutPlatform = previousPlatform;
+ Util.screenWidth = previousWidth;
+ Util.isIpad = previousIsIpad;
+ });
+ });
+
+ for (final (os, width, ipad, desktop) in <(String, double?, bool, bool)>[
+ ('android', 390, false, false),
+ ('android', 1000, false, false),
+ ('ios', 390, false, false),
+ ('ios', 1024, false, false),
+ ('ios', 390, true, true),
+ ('ios', 1024, true, true),
+ ('macos', 390, false, true),
+ ('windows', 390, false, true),
+ ('linux', 799, false, false),
+ ('linux', 800, false, true),
+ ('linux', null, false, true),
+ ]) {
+ test('layout for $os at width $width with isIpad=$ipad', () {
+ Util.layoutPlatform = platform.FakePlatform(operatingSystem: os);
+ Util.screenWidth = width;
+ Util.isIpad = ipad;
+
+ expect(Util.isDesktop, desktop);
+ });
+ }
+}
### test/wallets/spark_message_test.dart
@@ -0,0 +1,155 @@
+import 'dart:typed_data';
+
+import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib;
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+import 'package:stackwallet/db/isar/main_db.dart';
+import 'package:stackwallet/models/isar/models/isar_models.dart';
+import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
+import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart';
+import 'package:stackwallet/wl_gen/interfaces/lib_spark_interface.dart';
+
+class _AddressDB extends Mock implements MainDB {
+ Address? owned;
+
+ @override
+ Future<Address?> getAddress(String walletId, String address) async =>
+ owned?.value == address ? owned : null;
+}
+
+class _Wallet extends FiroWallet {
+ _Wallet() : super(CryptoCurrencyNetwork.main) {
+ mainDB = _AddressDB();
+ }
+
+ bool viewOnly = false;
+ @override
+ bool get isViewOnly => viewOnly;
+ @override
+ String get walletId => 'test';
+ @override
+ Future<coinlib.HDPrivateKey> getRootHDNode() async =>
+ coinlib.HDPrivateKey.fromSeed(Uint8List.fromList(List.filled(32, 1)));
+ @override
+ Future<Address?> getCurrentReceivingSparkAddress() async => null;
+}
+
+void main() {
+ setUpAll(coinlib.loadCoinlib);
+
+ test(
+ 'Spark sign/verify uses ownership proofs and preserves message bytes',
+ () async {
+ final wallet = _Wallet();
+ final root = await wallet.getRootHDNode();
+ final address = Address(
+ walletId: wallet.walletId,
+ value: await libSpark.getAddress(
+ privateKey: root
+ .derivePath(wallet.sparkDerivationPath)
+ .privateKey
+ .data,
+ index: wallet.sparkIndex,
+ diversifier: 0,
+ ),
+ publicKey: [],
+ derivationIndex: 0,
+ derivationPath: DerivationPath()..value = wallet.sparkDerivationPath,
+ type: AddressType.spark,
+ subType: AddressSubType.receiving,
+ );
+ (wallet.mainDB as _AddressDB).owned = address;
+ const message = ' ownership\nchallenge ';
+ final proof = await wallet.signMessage(message, address: address);
+ expect(proof, hasLength(260));
+ expect(
+ await wallet.verifyMessage(
+ message,
+ address: address.value,
+ signature: proof,
+ ),
+ isTrue,
+ );
+ expect(
+ await wallet.verifyMessage(
+ message.trim(),
+ address: address.value,
+ signature: proof,
+ ),
+ isFalse,
+ );
+ expect(
+ await wallet.verifyMessage(
+ message,
+ address: address.value,
+ signature: '${proof}00',
+ ),
+ isFalse,
+ );
+ for (final blank in ['', ' ', '\n\t']) {
+ await expectLater(
+ wallet.signMessage(blank, address: address),
+ throwsException,
+ );
+ }
+ wallet.viewOnly = true;
+ await expectLater(
+ wallet.signMessage(message, address: address),
+ throwsException,
+ );
+ expect(
+ await wallet.verifyMessage(
+ message,
+ address: address.value,
+ signature: proof,
+ ),
+ isTrue,
+ );
+ wallet.viewOnly = false;
+ (wallet.mainDB as _AddressDB).owned = null;
+ await expectLater(
+ wallet.signMessage(message, address: address),
+ throwsException,
+ );
+ },
+ );
+
+ test(
+ 'transparent signing and verification retain the Bitcoin-style path',
+ () async {
+ final wallet = _Wallet();
+ const path = "m/44'/136'/0'/0/0";
+ final key = (await wallet.getRootHDNode()).derivePath(path).publicKey;
+ final value = coinlib.P2PKHAddress.fromPublicKey(
+ key,
+ version: wallet.cryptoCurrency.networkParams.p2pkhPrefix,
+ ).toString();
+ final address = Address(
+ walletId: wallet.walletId,
+ value: value,
+ publicKey: key.data,
+ derivationIndex: 0,
+ derivationPath: DerivationPath()..value = path,
+ type: AddressType.p2pkh,
+ subType: AddressSubType.receiving,
+ );
+ final signature = await wallet.signMessage('message', address: address);
+ expect(
+ await wallet.verifyMessage(
+ 'message',
+ address: value,
+ signature: signature,
+ ),
+ isTrue,
+ );
+ expect(
+ await wallet.verifyMessage(
+ 'different',
+ address: value,
+ signature: signature,
+ ),
+ isFalse,
+ );
+ },
+ );
+}
### test/widgets/adaptive_text_field_test.dart
@@ -9,6 +9,44 @@ import "package:stackwallet/widgets/textfield_icon_button.dart";
import "../sample_data/theme_json.dart";
void main() {
+ testWidgets("message paste preserves exact whitespace", (tester) async {
+ final controller = TextEditingController();
+ addTearDown(controller.dispose);
+ const message = " challenge\n ";
+ final messenger =
+ TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
+ messenger.setMockMethodCallHandler(SystemChannels.platform, (call) async {
+ return call.method == "Clipboard.getData"
+ ? <String, dynamic>{"text": message}
+ : null;
+ });
+ addTearDown(
+ () => messenger.setMockMethodCallHandler(SystemChannels.platform, null),
+ );
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: ThemeData(
+ extensions: [
+ StackColors.fromStackColorTheme(
+ StackTheme.fromJson(json: lightThemeJsonMap),
+ ),
+ ],
+ ),
+ home: Scaffold(
+ body: AdaptiveTextField(
+ controller: controller,
+ showPasteClearButton: true,
+ trimPastedText: false,
+ maxLines: 5,
+ ),
+ ),
+ ),
+ );
+ await tester.tap(find.byType(TextFieldIconButton));
+ await tester.pump();
+ expect(controller.text, message);
+ });
+
testWidgets("paste trims whitespace and runs input formatters", (
tester,
) async {
### tool/wl_templates/FIRO_lib_spark_interface_impl.template.dart
@@ -139,6 +139,32 @@ class _LibSparkInterfaceImpl extends LibSparkInterface {
ignoreProof: ignoreProof,
);
+ @override
+ String createSparkAddressOwnershipProof({
+ required String message,
+ required String privateKeyHex,
+ required int spendKeyIndex,
+ required int diversifier,
+ }) => LibSpark.createSparkAddressOwnershipProof(
+ message: message,
+ privateKeyHex: privateKeyHex,
+ spendKeyIndex: spendKeyIndex,
+ diversifier: diversifier,
+ );
+
+ @override
+ bool verifySparkAddressOwnershipProof({
+ required String message,
+ required String address,
+ required String proof,
+ required bool isTestNet,
+ }) => LibSpark.verifySparkAddressOwnershipProof(
+ message: message,
+ address: address,
+ proof: proof,
+ isTestNet: isTestNet,
+ );
+
@override
Uint8List getSparkNameCommitment({
required Uint8List serializedSparkNameData,Why this scored 32/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.