From 3111bcf1f04823bed783c2e7f8d9566f231eb835 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 30 Oct 2026 17:32:18 -0500 Subject: [PATCH 1/5] Fix empty desktop address book chooser by creating the My Stack contact on open --- .../desktop_address_book.dart | 35 ++----------- .../address_book_address_chooser.dart | 18 +++++-- lib/services/address_book_service.dart | 49 +++++++++++++++++++ 3 files changed, 67 insertions(+), 35 deletions(-) diff --git a/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart index a4e767048b..5639b63079 100644 --- a/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart @@ -13,8 +13,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import '../../app_config.dart'; -import '../../models/isar/models/blockchain_data/address.dart'; -import '../../models/isar/models/contact_entry.dart'; import '../../pages/address_book_views/subviews/add_address_book_entry_view.dart'; import '../../pages/address_book_views/subviews/address_book_filter_view.dart'; import '../../providers/global/address_book_service_provider.dart'; @@ -26,7 +24,6 @@ import '../../utilities/constants.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; -import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../widgets/address_book_card.dart'; import '../../widgets/desktop/desktop_app_bar.dart'; import '../../widgets/desktop/desktop_dialog.dart'; @@ -119,35 +116,9 @@ class _DesktopAddressBook extends ConsumerState { // } WidgetsBinding.instance.addPostFrameCallback((_) async { - final List addresses = []; - final wallets = ref.read(pWallets).wallets; - for (final wallet in wallets) { - final String addressString; - if (wallet is SparkInterface && - !(wallet.isViewOnly && wallet.viewOnlyType != .spark)) { - Address? address = await wallet.getCurrentReceivingSparkAddress(); - address ??= await wallet.generateNextSparkAddress(saveToDB: true); - addressString = address.value; - } else { - final address = await wallet.getCurrentReceivingAddress(); - addressString = address?.value ?? wallet.info.cachedReceivingAddress; - } - - addresses.add( - ContactAddressEntry() - ..coinName = wallet.info.coin.identifier - ..address = addressString - ..label = "Current Receiving" - ..other = wallet.info.name, - ); - } - final self = ContactEntry( - name: "My ${AppConfig.prefix}", - addresses: addresses, - isFavorite: true, - customId: "default", - ); - await ref.read(addressBookServiceProvider).editContact(self); + await ref + .read(addressBookServiceProvider) + .updateMyStackContact(ref.read(pWallets).wallets); }); super.initState(); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart index 5e1252be4c..68fde1d4d4 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart @@ -14,6 +14,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import '../../../../../models/isar/models/contact_entry.dart'; import '../../../../../providers/global/address_book_service_provider.dart'; +import '../../../../../providers/providers.dart'; import '../../../../../themes/stack_colors.dart'; import '../../../../../utilities/assets.dart'; import '../../../../../utilities/constants.dart'; @@ -25,7 +26,7 @@ import '../../../../../widgets/stack_text_field.dart'; import '../../../../../widgets/textfield_icon_button.dart'; import 'sub_widgets/contact_list_item.dart'; -class AddressBookAddressChooser extends StatefulWidget { +class AddressBookAddressChooser extends ConsumerStatefulWidget { const AddressBookAddressChooser({ super.key, this.coin, @@ -34,11 +35,12 @@ class AddressBookAddressChooser extends StatefulWidget { final CryptoCurrency? coin; @override - State createState() => + ConsumerState createState() => _AddressBookAddressChooserState(); } -class _AddressBookAddressChooserState extends State { +class _AddressBookAddressChooserState + extends ConsumerState { late final bool isDesktop; late final TextEditingController _searchController; late final FocusNode searchFieldFocusNode; @@ -109,6 +111,16 @@ class _AddressBookAddressChooserState extends State { isDesktop = Util.isDesktop; searchFieldFocusNode = FocusNode(); _searchController = TextEditingController(); + + // the "My Stack" contact is otherwise only created by the address book + // view, so make sure it exists and is current before choosing from it + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!mounted) return; + await ref + .read(addressBookServiceProvider) + .updateMyStackContact(ref.read(pWallets).wallets); + }); + super.initState(); } diff --git a/lib/services/address_book_service.dart b/lib/services/address_book_service.dart index f49d9e6649..d550742367 100644 --- a/lib/services/address_book_service.dart +++ b/lib/services/address_book_service.dart @@ -10,8 +10,13 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; +import '../app_config.dart'; import '../db/isar/main_db.dart'; +import '../models/isar/models/blockchain_data/address.dart'; import '../models/isar/models/contact_entry.dart'; +import '../utilities/logger.dart'; +import '../wallets/wallet/wallet.dart'; +import '../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; class AddressBookService extends ChangeNotifier { ContactEntry getContactById(String id) { @@ -78,6 +83,50 @@ class AddressBookService extends ChangeNotifier { return true; } + /// Rebuilds the "My Stack" contact from the current receiving address of + /// each of [wallets]. + Future updateMyStackContact(Iterable wallets) async { + final List addresses = []; + for (final wallet in wallets) { + try { + final String addressString; + if (wallet is SparkInterface && + !(wallet.isViewOnly && wallet.viewOnlyType != .spark)) { + Address? address = await wallet.getCurrentReceivingSparkAddress(); + address ??= await wallet.generateNextSparkAddress(saveToDB: true); + addressString = address.value; + } else { + final address = await wallet.getCurrentReceivingAddress(); + addressString = address?.value ?? wallet.info.cachedReceivingAddress; + } + + addresses.add( + ContactAddressEntry() + ..coinName = wallet.info.coin.identifier + ..address = addressString + ..label = "Current Receiving" + ..other = wallet.info.name, + ); + } catch (e, s) { + // don't let one wallet keep the rest out of the address book + Logging.instance.e( + "Failed to get address for ${wallet.info.name}", + error: e, + stackTrace: s, + ); + } + } + + await editContact( + ContactEntry( + name: "My ${AppConfig.prefix}", + addresses: addresses, + isFavorite: true, + customId: "default", + ), + ); + } + /// Remove address book contact entry from db if it exists Future removeContact(String id) async { await MainDB.instance.deleteContactEntry(id: id); From 05c762fa66958557f2347da17d96d7d9dab272c0 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sun, 1 Nov 2026 14:19:39 -0600 Subject: [PATCH 2/5] fix(address book): preserve complete My Stack updates --- lib/services/address_book_service.dart | 16 +++- test/services/address_book_service_test.dart | 97 ++++++++++++++++++++ 2 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 test/services/address_book_service_test.dart diff --git a/lib/services/address_book_service.dart b/lib/services/address_book_service.dart index d550742367..f1110fbb32 100644 --- a/lib/services/address_book_service.dart +++ b/lib/services/address_book_service.dart @@ -10,6 +10,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; +import 'package:mutex/mutex.dart'; import '../app_config.dart'; import '../db/isar/main_db.dart'; import '../models/isar/models/blockchain_data/address.dart'; @@ -19,6 +20,8 @@ import '../wallets/wallet/wallet.dart'; import '../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; class AddressBookService extends ChangeNotifier { + final Mutex _myStackContactLock = Mutex(); + ContactEntry getContactById(String id) { final ContactEntry? contactEntry = MainDB.instance.getContactEntry(id: id); if (contactEntry == null) { @@ -85,8 +88,12 @@ class AddressBookService extends ChangeNotifier { /// Rebuilds the "My Stack" contact from the current receiving address of /// each of [wallets]. - Future updateMyStackContact(Iterable wallets) async { + Future updateMyStackContact(Iterable wallets) => + _myStackContactLock.protect(() => _updateMyStackContact(wallets)); + + Future _updateMyStackContact(Iterable wallets) async { final List addresses = []; + var failed = false; for (final wallet in wallets) { try { final String addressString; @@ -108,15 +115,18 @@ class AddressBookService extends ChangeNotifier { ..other = wallet.info.name, ); } catch (e, s) { - // don't let one wallet keep the rest out of the address book + failed = true; Logging.instance.e( - "Failed to get address for ${wallet.info.name}", + "Failed to rebuild My Stack contact", error: e, stackTrace: s, ); } } + // Keep the last complete contact if any wallet could not be refreshed. + if (failed) return; + await editContact( ContactEntry( name: "My ${AppConfig.prefix}", diff --git a/test/services/address_book_service_test.dart b/test/services/address_book_service_test.dart new file mode 100644 index 0000000000..fe5ad68536 --- /dev/null +++ b/test/services/address_book_service_test.dart @@ -0,0 +1,97 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/isar/models/contact_entry.dart'; +import 'package:stackwallet/services/address_book_service.dart'; +import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; +import 'package:stackwallet/wallets/wallet/wallet.dart'; + +class _TestAddressBookService extends AddressBookService { + final writes = []; + + @override + Future editContact(ContactEntry editedContact) async { + writes.add(editedContact); + return true; + } +} + +class _TestWallet extends Mock implements Wallet { + _TestWallet({required this.info, required this.loadAddress}); + + @override + final WalletInfo info; + + final Future Function() loadAddress; + + @override + Future getCurrentReceivingAddress() => loadAddress(); +} + +WalletInfo _walletInfo(String id) => WalletInfo( + walletId: id, + name: id, + mainAddressType: AddressType.p2wpkh, + coinName: "bitcoin", + cachedReceivingAddress: "cached-$id", +); + +Address _address(String walletId) => Address( + walletId: walletId, + value: "address-$walletId", + publicKey: const [], + derivationIndex: 0, + derivationPath: null, + type: AddressType.p2wpkh, + subType: AddressSubType.receiving, +); + +void main() { + test("does not replace My Stack with a partial contact", () async { + final service = _TestAddressBookService(); + final goodWallet = _TestWallet( + info: _walletInfo("good"), + loadAddress: () async => _address("good"), + ); + final failingWallet = _TestWallet( + info: _walletInfo("failing"), + loadAddress: () async => throw Exception("temporary failure"), + ); + + await service.updateMyStackContact([goodWallet, failingWallet]); + + expect(service.writes, isEmpty); + }); + + test("serializes My Stack updates in call order", () async { + final service = _TestAddressBookService(); + final firstStarted = Completer(); + final firstAddress = Completer(); + final firstWallet = _TestWallet( + info: _walletInfo("first"), + loadAddress: () { + firstStarted.complete(); + return firstAddress.future; + }, + ); + final secondWallet = _TestWallet( + info: _walletInfo("second"), + loadAddress: () async => _address("second"), + ); + + final firstUpdate = service.updateMyStackContact([firstWallet]); + await firstStarted.future; + final secondUpdate = service.updateMyStackContact([secondWallet]); + await Future.delayed(Duration.zero); + + expect(service.writes, isEmpty); + + firstAddress.complete(_address("first")); + await Future.wait([firstUpdate, secondUpdate]); + + expect(service.writes, hasLength(2)); + expect(service.writes.last.addresses.single.address, "address-second"); + }); +} From bb10b802402d7275e38fb95bbe2c4455cab7ea54 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sun, 1 Nov 2026 14:45:53 -0600 Subject: [PATCH 3/5] fix(address book): use a wallet's cached address when its lookup fails Skipping the whole My Stack update when one wallet failed meant a wallet that always fails (for example one whose native library won't load) kept the contact from ever being created or updated, which is the empty chooser this branch fixes. Fall back to that wallet's cached receiving address instead, and leave out wallets with no address at all. --- lib/services/address_book_service.dart | 29 ++++++++++---------- test/services/address_book_service_test.dart | 8 ++++-- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/services/address_book_service.dart b/lib/services/address_book_service.dart index f1110fbb32..6427ee1cde 100644 --- a/lib/services/address_book_service.dart +++ b/lib/services/address_book_service.dart @@ -93,10 +93,9 @@ class AddressBookService extends ChangeNotifier { Future _updateMyStackContact(Iterable wallets) async { final List addresses = []; - var failed = false; for (final wallet in wallets) { + String addressString; try { - final String addressString; if (wallet is SparkInterface && !(wallet.isViewOnly && wallet.viewOnlyType != .spark)) { Address? address = await wallet.getCurrentReceivingSparkAddress(); @@ -106,27 +105,27 @@ class AddressBookService extends ChangeNotifier { final address = await wallet.getCurrentReceivingAddress(); addressString = address?.value ?? wallet.info.cachedReceivingAddress; } - - addresses.add( - ContactAddressEntry() - ..coinName = wallet.info.coin.identifier - ..address = addressString - ..label = "Current Receiving" - ..other = wallet.info.name, - ); } catch (e, s) { - failed = true; + // Fall back to the last address saved for this wallet, so one failing + // wallet doesn't drop out of the contact or keep the rest from updating. Logging.instance.e( - "Failed to rebuild My Stack contact", + "Failed to get a wallet address for My Stack, using the cached one", error: e, stackTrace: s, ); + addressString = wallet.info.cachedReceivingAddress; } + if (addressString.isEmpty) continue; + + addresses.add( + ContactAddressEntry() + ..coinName = wallet.info.coin.identifier + ..address = addressString + ..label = "Current Receiving" + ..other = wallet.info.name, + ); } - // Keep the last complete contact if any wallet could not be refreshed. - if (failed) return; - await editContact( ContactEntry( name: "My ${AppConfig.prefix}", diff --git a/test/services/address_book_service_test.dart b/test/services/address_book_service_test.dart index fe5ad68536..107eb7811f 100644 --- a/test/services/address_book_service_test.dart +++ b/test/services/address_book_service_test.dart @@ -49,7 +49,7 @@ Address _address(String walletId) => Address( ); void main() { - test("does not replace My Stack with a partial contact", () async { + test("uses the cached address for a wallet that fails", () async { final service = _TestAddressBookService(); final goodWallet = _TestWallet( info: _walletInfo("good"), @@ -62,7 +62,11 @@ void main() { await service.updateMyStackContact([goodWallet, failingWallet]); - expect(service.writes, isEmpty); + expect(service.writes, hasLength(1)); + expect(service.writes.single.addresses.map((e) => e.address), [ + "address-good", + "cached-failing", + ]); }); test("serializes My Stack updates in call order", () async { From d1697eaee7fbe9434c211a117ece83c90f47e85b Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 3 Nov 2026 10:25:11 -0600 Subject: [PATCH 4/5] style: dart format --- .../desktop_address_book.dart | 24 ++-- .../address_book_address_chooser.dart | 124 ++++++++---------- lib/services/address_book_service.dart | 1 + 3 files changed, 69 insertions(+), 80 deletions(-) diff --git a/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart index 5639b63079..944aed6be7 100644 --- a/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart @@ -257,9 +257,9 @@ class _DesktopAddressBook extends ConsumerState { buttonHeight: ButtonHeight.l, icon: SvgPicture.asset( Assets.svg.filter, - color: Theme.of( - context, - ).extension()!.buttonTextSecondary, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, ), onPressed: selectCryptocurrency, ), @@ -270,9 +270,9 @@ class _DesktopAddressBook extends ConsumerState { buttonHeight: ButtonHeight.l, icon: SvgPicture.asset( Assets.svg.circlePlus, - color: Theme.of( - context, - ).extension()!.buttonTextPrimary, + color: Theme.of(context) + .extension()! + .buttonTextPrimary, ), onPressed: newContact, ), @@ -314,9 +314,9 @@ class _DesktopAddressBook extends ConsumerState { children: [ if (i > 0) Container( - color: Theme.of( - context, - ).extension()!.background, + color: Theme.of(context) + .extension()! + .background, height: 1, ), Padding( @@ -385,9 +385,9 @@ class _DesktopAddressBook extends ConsumerState { children: [ if (i > 0) Container( - color: Theme.of( - context, - ).extension()!.background, + color: Theme.of(context) + .extension()! + .background, height: 1, ), Padding( diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart index 68fde1d4d4..11fb09e7c4 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart @@ -27,10 +27,7 @@ import '../../../../../widgets/textfield_icon_button.dart'; import 'sub_widgets/contact_list_item.dart'; class AddressBookAddressChooser extends ConsumerStatefulWidget { - const AddressBookAddressChooser({ - super.key, - this.coin, - }); + const AddressBookAddressChooser({super.key, this.coin}); final CryptoCurrency? coin; @@ -137,9 +134,7 @@ class _AddressBookAddressChooserState children: [ // search field Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), + padding: const EdgeInsets.symmetric(horizontal: 32), child: ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -162,63 +157,59 @@ class _AddressBookAddressChooserState height: 1.8, ) : STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - searchFieldFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - prefixIcon: Padding( - padding: EdgeInsets.symmetric( - horizontal: isDesktop ? 12 : 10, - vertical: isDesktop ? 18 : 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: isDesktop ? 20 : 16, - height: isDesktop ? 20 : 16, - ), - ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - _searchTerm = ""; - }); - }, + decoration: + standardInputDecoration( + "Search", + searchFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: EdgeInsets.symmetric( + horizontal: isDesktop ? 12 : 10, + vertical: isDesktop ? 18 : 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: isDesktop ? 20 : 16, + height: isDesktop ? 20 : 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], ), - ], - ), - ), - ) - : null, - ), + ), + ) + : null, + ), ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Expanded( child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), + padding: const EdgeInsets.only(left: 32, right: 32, bottom: 32), child: Consumer( builder: (context, ref, _) { List contacts = ref .watch( - addressBookServiceProvider - .select((value) => value.contacts), + addressBookServiceProvider.select( + (value) => value.contacts, + ), ) .toList(); @@ -226,7 +217,8 @@ class _AddressBookAddressChooserState final favorites = pullOutFavorites(contacts); - final totalLength = favorites.length + + final totalLength = + favorites.length + contacts.length + 2; // +2 for "fav" and "all" headers @@ -235,9 +227,7 @@ class _AddressBookAddressChooserState shrinkWrap: true, itemCount: totalLength, separatorBuilder: (context, index) { - return const SizedBox( - height: 10, - ); + return const SizedBox(height: 10); }, itemBuilder: (context, index) { if (index == 0) { @@ -245,13 +235,12 @@ class _AddressBookAddressChooserState key: const Key( "addressBookCAddressChooserFavoritesHeaderItemKey", ), - padding: const EdgeInsets.only( - bottom: 10, - ), + padding: const EdgeInsets.only(bottom: 10), child: Text( "Favorites", - style: - STextStyles.desktopTextExtraExtraSmall(context), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), ), ); } else if (index < favorites.length + 1) { @@ -266,13 +255,12 @@ class _AddressBookAddressChooserState key: const Key( "addressBookCAddressChooserAllContactsHeaderItemKey", ), - padding: const EdgeInsets.symmetric( - vertical: 10, - ), + padding: const EdgeInsets.symmetric(vertical: 10), child: Text( "All contacts", - style: - STextStyles.desktopTextExtraExtraSmall(context), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), ), ); } else { diff --git a/lib/services/address_book_service.dart b/lib/services/address_book_service.dart index 6427ee1cde..3526bd4cbd 100644 --- a/lib/services/address_book_service.dart +++ b/lib/services/address_book_service.dart @@ -11,6 +11,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:mutex/mutex.dart'; + import '../app_config.dart'; import '../db/isar/main_db.dart'; import '../models/isar/models/blockchain_data/address.dart'; From eefcf5d35f2a178a94e360c164a6af38a3124ad3 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 3 Nov 2026 12:13:16 -0600 Subject: [PATCH 5/5] fix(address book): leave out a Spark wallet whose Spark address fails A Firo wallet's cached receiving address is its transparent one, so falling back to it put a public address in My Stack where a Spark address belonged. --- lib/services/address_book_service.dart | 23 ++++++---- test/services/address_book_service_test.dart | 44 ++++++++++++++++++++ 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/lib/services/address_book_service.dart b/lib/services/address_book_service.dart index 3526bd4cbd..cdf794af78 100644 --- a/lib/services/address_book_service.dart +++ b/lib/services/address_book_service.dart @@ -95,25 +95,34 @@ class AddressBookService extends ChangeNotifier { Future _updateMyStackContact(Iterable wallets) async { final List addresses = []; for (final wallet in wallets) { + final SparkInterface? sparkWallet = + wallet is SparkInterface && + !(wallet.isViewOnly && wallet.viewOnlyType != .spark) + ? wallet + : null; String addressString; try { - if (wallet is SparkInterface && - !(wallet.isViewOnly && wallet.viewOnlyType != .spark)) { - Address? address = await wallet.getCurrentReceivingSparkAddress(); - address ??= await wallet.generateNextSparkAddress(saveToDB: true); + if (sparkWallet != null) { + Address? address = await sparkWallet + .getCurrentReceivingSparkAddress(); + address ??= await sparkWallet.generateNextSparkAddress( + saveToDB: true, + ); addressString = address.value; } else { final address = await wallet.getCurrentReceivingAddress(); addressString = address?.value ?? wallet.info.cachedReceivingAddress; } } catch (e, s) { - // Fall back to the last address saved for this wallet, so one failing - // wallet doesn't drop out of the contact or keep the rest from updating. + // A Spark wallet's cached address is its transparent one, so leave it + // out rather than offer a public address. Other wallets fall back to + // the cached address so one failure doesn't drop them or stop the rest. Logging.instance.e( - "Failed to get a wallet address for My Stack, using the cached one", + "Failed to get a wallet address for My Stack", error: e, stackTrace: s, ); + if (sparkWallet != null) continue; addressString = wallet.info.cachedReceivingAddress; } if (addressString.isEmpty) continue; diff --git a/test/services/address_book_service_test.dart b/test/services/address_book_service_test.dart index 107eb7811f..bdd39cabfd 100644 --- a/test/services/address_book_service_test.dart +++ b/test/services/address_book_service_test.dart @@ -5,8 +5,10 @@ import 'package:mockito/mockito.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/models/isar/models/contact_entry.dart'; import 'package:stackwallet/services/address_book_service.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart'; import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; class _TestAddressBookService extends AddressBookService { final writes = []; @@ -30,6 +32,24 @@ class _TestWallet extends Mock implements Wallet { Future getCurrentReceivingAddress() => loadAddress(); } +class _TestSparkWallet extends Mock + implements Wallet, SparkInterface { + _TestSparkWallet({required this.info}); + + @override + final WalletInfo info; + + @override + bool get isViewOnly => false; + + @override + Future getCurrentReceivingSparkAddress() async => null; + + @override + Future
generateNextSparkAddress({required bool saveToDB}) => + throw Exception("spark address generation failed"); +} + WalletInfo _walletInfo(String id) => WalletInfo( walletId: id, name: id, @@ -69,6 +89,30 @@ void main() { ]); }); + test("leaves out a Spark wallet whose Spark address fails", () async { + final service = _TestAddressBookService(); + final goodWallet = _TestWallet( + info: _walletInfo("good"), + loadAddress: () async => _address("good"), + ); + final sparkWallet = _TestSparkWallet( + info: WalletInfo( + walletId: "firo", + name: "firo", + mainAddressType: AddressType.p2pkh, + coinName: "firo", + cachedReceivingAddress: "transparent-firo", + ), + ); + + await service.updateMyStackContact([goodWallet, sparkWallet]); + + expect(service.writes, hasLength(1)); + expect(service.writes.single.addresses.map((e) => e.address), [ + "address-good", + ]); + }); + test("serializes My Stack updates in call order", () async { final service = _TestAddressBookService(); final firstStarted = Completer();