From 64f27d3f538e0de9ed8f6e28dd93db13b6c41ad7 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Thu, 23 Jul 2026 17:22:36 +0700 Subject: [PATCH 1/3] fix: validate SS58 in deep links and repair checkphrase failure path (L9) - /pay deep links now validate the recipient with isValidSS58Address before pre-filling the send flow; invalid links are dropped - Shared-account sheet validates the address before navigating to send, showing an error toast and failing closed on invalid input - HumanReadableChecksumService.getHumanReadableName returns null instead of '' on error so the _recipientChecksum != null gate works, and cache eviction now uses the same '#U'-suffixed key as the cache store Addresses finding L9 of the 2026-07-22 mobile wallet security audit. --- .../lib/providers/wallet_providers.dart | 3 ++- .../components/shared_address_action_sheet.dart | 14 ++++++++++++-- mobile-app/lib/providers/wallet_providers.dart | 5 +++-- mobile-app/lib/services/deep_link_service.dart | 7 +++++-- mobile-app/lib/services/referral_service.dart | 2 +- .../v2/screens/send/select_recipient_screen.dart | 2 +- .../services/human_readable_checksum_service.dart | 14 ++++++++------ 7 files changed, 32 insertions(+), 15 deletions(-) diff --git a/cold-wallet-app/lib/providers/wallet_providers.dart b/cold-wallet-app/lib/providers/wallet_providers.dart index a41d0512a..fa75c9cfd 100644 --- a/cold-wallet-app/lib/providers/wallet_providers.dart +++ b/cold-wallet-app/lib/providers/wallet_providers.dart @@ -114,5 +114,6 @@ final addressProvider = Provider((ref) => ref.watch(keypairProvider)?.s final checkphraseProvider = FutureProvider((ref) async { final address = ref.watch(addressProvider); if (address == null) return ''; - return HumanReadableChecksumService().getHumanReadableName(address); + final name = await HumanReadableChecksumService().getHumanReadableName(address); + return name ?? ''; }); diff --git a/mobile-app/lib/features/components/shared_address_action_sheet.dart b/mobile-app/lib/features/components/shared_address_action_sheet.dart index c9d583af7..118c6e4ee 100644 --- a/mobile-app/lib/features/components/shared_address_action_sheet.dart +++ b/mobile-app/lib/features/components/shared_address_action_sheet.dart @@ -5,10 +5,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; +import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/routes.dart'; import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; import 'package:resonance_network_wallet/shared/extensions/current_route_extensions.dart'; import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; +import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/screens/send/input_amount_screen.dart'; import 'package:resonance_network_wallet/v2/screens/send/keystone_sign_cache.dart'; @@ -24,7 +27,7 @@ class SharedAddressActionSheet extends StatefulWidget { class _SharedAddressActionSheetState extends State { String? _checksum; - Future? _checksumFuture; + Future? _checksumFuture; List? _splittedAddress; final HumanReadableChecksumService _checksumService = HumanReadableChecksumService(); @@ -60,7 +63,14 @@ class _SharedAddressActionSheetState extends State { } void _sendToAddress() { - ProviderScope.containerOf(context).read(keystoneSignCacheProvider.notifier).startNewSendSession(); + final container = ProviderScope.containerOf(context); + // Fail closed: never pre-fill the send flow with an invalid address, + // same as address entry in the send flow itself. + if (!container.read(substrateServiceProvider).isValidSS58Address(widget.address)) { + context.showErrorToaster(message: container.read(l10nProvider).addHardwareAccountInvalidAddress); + return; + } + container.read(keystoneSignCacheProvider.notifier).startNewSendSession(); Navigator.of(context).pop(); Navigator.push( context, diff --git a/mobile-app/lib/providers/wallet_providers.dart b/mobile-app/lib/providers/wallet_providers.dart index c2ebab44b..64a2e4d6b 100644 --- a/mobile-app/lib/providers/wallet_providers.dart +++ b/mobile-app/lib/providers/wallet_providers.dart @@ -46,8 +46,9 @@ final humanReadableChecksumServiceProvider = Provider((ref, address) { - return ref.watch(humanReadableChecksumServiceProvider).getHumanReadableName(address); +final checksumNameProvider = FutureProvider.family((ref, address) async { + final name = await ref.watch(humanReadableChecksumServiceProvider).getHumanReadableName(address); + return name ?? ''; }); final reversibleTransfersServiceProvider = Provider((ref) { diff --git a/mobile-app/lib/services/deep_link_service.dart b/mobile-app/lib/services/deep_link_service.dart index 96cc6808a..dd0d465f1 100644 --- a/mobile-app/lib/services/deep_link_service.dart +++ b/mobile-app/lib/services/deep_link_service.dart @@ -3,6 +3,7 @@ import 'package:app_links/app_links.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:resonance_network_wallet/providers/account_associations_providers.dart'; import 'package:resonance_network_wallet/providers/route_intent_providers.dart'; +import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/shared/utils/print.dart'; final deepLinkServiceProvider = Provider((ref) { @@ -56,10 +57,12 @@ class DeepLinkService { if (uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'pay') { final payment = PaymentIntent.tryParseUrl(uri.toString()); - if (payment != null) { + // Fail closed: a /pay link with an invalid recipient must not pre-fill + // the send flow, same as address entry in the send flow itself. + if (payment != null && _ref.read(substrateServiceProvider).isValidSS58Address(payment.to)) { _ref.read(paymentIntentProvider.notifier).state = payment; } else { - quantusDebugPrint('Missing payment parameters'); + quantusDebugPrint('Missing payment parameters or invalid recipient address'); } } diff --git a/mobile-app/lib/services/referral_service.dart b/mobile-app/lib/services/referral_service.dart index 19e2930e2..3932aef9b 100644 --- a/mobile-app/lib/services/referral_service.dart +++ b/mobile-app/lib/services/referral_service.dart @@ -144,7 +144,7 @@ class ReferralService { final account = await getMainAccount(); final referralCode = await _checksumService.getHumanReadableName(account.accountId); - return referralCode; + return referralCode ?? ''; } Future getShareLinkParameters(Rect? positionOrigin) async { diff --git a/mobile-app/lib/v2/screens/send/select_recipient_screen.dart b/mobile-app/lib/v2/screens/send/select_recipient_screen.dart index 98bff444d..53f073c93 100644 --- a/mobile-app/lib/v2/screens/send/select_recipient_screen.dart +++ b/mobile-app/lib/v2/screens/send/select_recipient_screen.dart @@ -79,7 +79,7 @@ class _SelectRecipientScreenState extends ConsumerState { }); for (final addr in addresses) { checksumService.getHumanReadableName(addr).then((name) { - if (mounted) setState(() => _checksums[addr] = name); + if (mounted && name != null) setState(() => _checksums[addr] = name); }); } } catch (e) { diff --git a/quantus_sdk/lib/src/services/human_readable_checksum_service.dart b/quantus_sdk/lib/src/services/human_readable_checksum_service.dart index d6cf28f2c..a1d877f60 100644 --- a/quantus_sdk/lib/src/services/human_readable_checksum_service.dart +++ b/quantus_sdk/lib/src/services/human_readable_checksum_service.dart @@ -59,9 +59,9 @@ class HumanReadableChecksumService { } } - Future getHumanReadableName(String address, {upperCase = true}) async { + Future getHumanReadableName(String address, {upperCase = true}) async { + final key = address + (upperCase ? '#U' : ''); try { - final key = address + (upperCase ? '#U' : ''); if (_checkPhraseCache.containsKey(key)) { return _checkPhraseCache[key]!; } @@ -72,7 +72,7 @@ class HumanReadableChecksumService { if (_isolateSendPort == null) { debugPrint('Error: _isolateSendPort is null after successful initialization wait.'); - return ''; + return null; } final responsePort = ReceivePort(); @@ -80,7 +80,9 @@ class HumanReadableChecksumService { final result = await responsePort.first as String?; responsePort.close(); - var finalResult = result ?? ''; + if (result == null || result.isEmpty) return null; + + var finalResult = result; if (upperCase) { finalResult = finalResult @@ -94,8 +96,8 @@ class HumanReadableChecksumService { } catch (e, s) { debugPrint('Error in getHumanReadableName for address $address: $e'); debugPrint('Lookup error stack: $s'); - _checkPhraseCache.remove(address); - return ''; + _checkPhraseCache.remove(key); + return null; } } From 092156e25da7533d10884c7b97b395a5c1afb671 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Tue, 28 Jul 2026 17:34:05 +0800 Subject: [PATCH 2/3] fix: address L9 review findings - receive screen: degrade to blank checkphrase on lookup failure instead of an unbounded loader - use a neutral invalidAddress l10n key in the shared-address sheet instead of the add-hardware-account key - checksum service: swallow abandoned completer future on init failure so it no longer surfaces as an unhandled async error - add unit/widget tests for PaymentIntent parsing, /pay deep-link fail-closed behavior, checkphrase service, and the shared-address sheet send guard --- .../shared_address_action_sheet.dart | 2 +- mobile-app/lib/l10n/app_en.arb | 5 ++ mobile-app/lib/l10n/app_id.arb | 2 + mobile-app/lib/l10n/app_localizations.dart | 6 +++ mobile-app/lib/l10n/app_localizations_en.dart | 3 ++ mobile-app/lib/l10n/app_localizations_id.dart | 3 ++ .../lib/services/deep_link_service.dart | 8 +-- .../v2/screens/receive/receive_screen.dart | 4 +- .../test/unit/deep_link_service_test.dart | 53 +++++++++++++++++++ .../human_readable_checksum_service_test.dart | 35 ++++++++++++ mobile-app/test/unit/payment_intent_test.dart | 46 ++++++++++++++++ .../shared_address_action_sheet_test.dart | 47 ++++++++++++++++ .../human_readable_checksum_service.dart | 4 ++ .../human_readable_checksum_service_test.dart | 18 +++++++ 14 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 mobile-app/test/unit/deep_link_service_test.dart create mode 100644 mobile-app/test/unit/human_readable_checksum_service_test.dart create mode 100644 mobile-app/test/unit/payment_intent_test.dart create mode 100644 mobile-app/test/unit/shared_address_action_sheet_test.dart create mode 100644 quantus_sdk/test/human_readable_checksum_service_test.dart diff --git a/mobile-app/lib/features/components/shared_address_action_sheet.dart b/mobile-app/lib/features/components/shared_address_action_sheet.dart index 118c6e4ee..5d268fe77 100644 --- a/mobile-app/lib/features/components/shared_address_action_sheet.dart +++ b/mobile-app/lib/features/components/shared_address_action_sheet.dart @@ -67,7 +67,7 @@ class _SharedAddressActionSheetState extends State { // Fail closed: never pre-fill the send flow with an invalid address, // same as address entry in the send flow itself. if (!container.read(substrateServiceProvider).isValidSS58Address(widget.address)) { - context.showErrorToaster(message: container.read(l10nProvider).addHardwareAccountInvalidAddress); + context.showErrorToaster(message: container.read(l10nProvider).invalidAddress); return; } container.read(keystoneSignCacheProvider.notifier).startNewSendSession(); diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index 619d0e0aa..8ba21aa53 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -1282,6 +1282,11 @@ "description": "Validation when address is invalid" }, + "invalidAddress": "Invalid address", + "@invalidAddress": { + "description": "Shown when an address fails SS58 validation" + }, + "sendTitle": "Send", "@sendTitle": { "description": "Send flow app bar title" diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb index b7f26eabe..3301c26fa 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -292,6 +292,8 @@ "addHardwareAccountNameRequired": "Nama wajib diisi", "addHardwareAccountInvalidAddress": "Alamat tidak valid", + "invalidAddress": "Alamat tidak valid", + "sendTitle": "Kirim", "sendPayTitle": "Bayar", "sendEnterAddress": "Masukkan Alamat", diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index 9ad32e748..091ba73cb 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -1736,6 +1736,12 @@ abstract class AppLocalizations { /// **'Invalid address'** String get addHardwareAccountInvalidAddress; + /// Shown when an address fails SS58 validation + /// + /// In en, this message translates to: + /// **'Invalid address'** + String get invalidAddress; + /// Send flow app bar title /// /// In en, this message translates to: diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index a76e12d00..e7c2f60fc 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -924,6 +924,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get addHardwareAccountInvalidAddress => 'Invalid address'; + @override + String get invalidAddress => 'Invalid address'; + @override String get sendTitle => 'Send'; diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart index 72cb6060f..892a128c7 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -921,6 +921,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get addHardwareAccountInvalidAddress => 'Alamat tidak valid'; + @override + String get invalidAddress => 'Alamat tidak valid'; + @override String get sendTitle => 'Kirim'; diff --git a/mobile-app/lib/services/deep_link_service.dart b/mobile-app/lib/services/deep_link_service.dart index dd0d465f1..05219301b 100644 --- a/mobile-app/lib/services/deep_link_service.dart +++ b/mobile-app/lib/services/deep_link_service.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:app_links/app_links.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:resonance_network_wallet/providers/account_associations_providers.dart'; import 'package:resonance_network_wallet/providers/route_intent_providers.dart'; @@ -24,18 +25,19 @@ class DeepLinkService { // Handle links when the app is already open (warm state) _linkSubscription = _appLinks.uriLinkStream.listen((uri) { quantusDebugPrint('Received link while app is open: $uri'); - _handleLink(uri); + handleLink(uri); }); // Handle the link that opened the app (cold state) final initialUri = await _appLinks.getInitialLink(); if (initialUri != null) { quantusDebugPrint('Received initial link: $initialUri'); - _handleLink(initialUri); + handleLink(initialUri); } } - void _handleLink(Uri uri) { + @visibleForTesting + void handleLink(Uri uri) { if (uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'account') { String? accountId; diff --git a/mobile-app/lib/v2/screens/receive/receive_screen.dart b/mobile-app/lib/v2/screens/receive/receive_screen.dart index 25b6312ed..fc007a1a3 100644 --- a/mobile-app/lib/v2/screens/receive/receive_screen.dart +++ b/mobile-app/lib/v2/screens/receive/receive_screen.dart @@ -55,7 +55,9 @@ class _ReceiveScreenState extends ConsumerState { final service = ref.read(encryptedAccountServiceProvider((base as Account).walletIndex)); accountId = (await service.receiveKeyPair()).address; } - final checksum = await checksumService.getHumanReadableName(accountId); + // Degrade to a blank checkphrase on lookup failure so the address/QR + // still renders instead of an unbounded loader. + final checksum = await checksumService.getHumanReadableName(accountId) ?? ''; if (!mounted) return; setState(() { _accountId = accountId; diff --git a/mobile-app/test/unit/deep_link_service_test.dart b/mobile-app/test/unit/deep_link_service_test.dart new file mode 100644 index 000000000..25a2f2826 --- /dev/null +++ b/mobile-app/test/unit/deep_link_service_test.dart @@ -0,0 +1,53 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:resonance_network_wallet/providers/route_intent_providers.dart'; +import 'package:resonance_network_wallet/services/deep_link_service.dart'; + +void main() { + group('DeepLinkService /pay handling', () { + late ProviderContainer container; + + setUp(() { + container = ProviderContainer(); + }); + + tearDown(() { + container.dispose(); + }); + + void handle(String url) { + container.read(deepLinkServiceProvider).handleLink(Uri.parse(url)); + } + + test('drops a /pay link with a malformed recipient', () { + handle('https://www.quantus.com/pay?to=not-a-valid-address&amount=1.5'); + + expect(container.read(paymentIntentProvider), isNull); + }); + + test('drops a /pay link with an invalid-checksum recipient', () { + // One character off from a valid address — must fail SS58 validation. + handle('https://www.quantus.com/pay?to=qzpyxSr48YN9EQe2ito734iCReTXjnungmNCSY4Yph1YznEdX&amount=1.5'); + + expect(container.read(paymentIntentProvider), isNull); + }); + + test('drops a /pay link with a missing recipient', () { + handle('https://www.quantus.com/pay?amount=1.5'); + + expect(container.read(paymentIntentProvider), isNull); + }); + + test('drops a /pay link with a missing amount', () { + handle('https://www.quantus.com/pay?to=qzpyxSr48YN9EQe2ito734iCReTXjnungmNCSY4Yph1YznEda'); + + expect(container.read(paymentIntentProvider), isNull); + }); + + test('ignores unrelated links', () { + handle('https://www.quantus.com/unknown?to=whatever&amount=1'); + + expect(container.read(paymentIntentProvider), isNull); + }); + }); +} diff --git a/mobile-app/test/unit/human_readable_checksum_service_test.dart b/mobile-app/test/unit/human_readable_checksum_service_test.dart new file mode 100644 index 000000000..fed032076 --- /dev/null +++ b/mobile-app/test/unit/human_readable_checksum_service_test.dart @@ -0,0 +1,35 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const validAddress = 'qzpyxSr48YN9EQe2ito734iCReTXjnungmNCSY4Yph1YznEda'; + + group('HumanReadableChecksumService.getHumanReadableName', () { + test('returns a checkphrase for a valid address', () async { + final name = await HumanReadableChecksumService().getHumanReadableName(validAddress); + + expect(name, isNotNull); + expect(name, isNotEmpty); + expect(name, contains('-')); + }); + + test('serves repeat lookups from cache', () async { + final first = await HumanReadableChecksumService().getHumanReadableName(validAddress); + final second = await HumanReadableChecksumService().getHumanReadableName(validAddress); + + expect(second, first); + }); + + test('is deterministic across addresses', () async { + final other = await HumanReadableChecksumService().getHumanReadableName( + 'qzjij4Tiow9jtse9d7L1T3NEZuxgFW8JdUbaTLsfgubF7ZQAC', + ); + final original = await HumanReadableChecksumService().getHumanReadableName(validAddress); + + expect(other, isNotNull); + expect(other, isNot(original)); + }); + }); +} diff --git a/mobile-app/test/unit/payment_intent_test.dart b/mobile-app/test/unit/payment_intent_test.dart new file mode 100644 index 000000000..6f9417b37 --- /dev/null +++ b/mobile-app/test/unit/payment_intent_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:resonance_network_wallet/providers/route_intent_providers.dart'; + +void main() { + group('PaymentIntent.tryParseUrl', () { + test('parses a valid /pay link', () { + final intent = PaymentIntent.tryParseUrl('https://www.quantus.com/pay?to=recipient&amount=1.5&ref=order-1'); + + expect(intent, isNotNull); + expect(intent!.to, 'recipient'); + expect(intent.amount, '1.5'); + expect(intent.ref, 'order-1'); + }); + + test('ref is optional', () { + final intent = PaymentIntent.tryParseUrl('https://www.quantus.com/pay?to=recipient&amount=1.5'); + + expect(intent, isNotNull); + expect(intent!.ref, isNull); + }); + + test('returns null when to is missing', () { + expect(PaymentIntent.tryParseUrl('https://www.quantus.com/pay?amount=1.5'), isNull); + }); + + test('returns null when to is empty', () { + expect(PaymentIntent.tryParseUrl('https://www.quantus.com/pay?to=&amount=1.5'), isNull); + }); + + test('returns null when amount is missing', () { + expect(PaymentIntent.tryParseUrl('https://www.quantus.com/pay?to=recipient'), isNull); + }); + + test('returns null when amount is empty', () { + expect(PaymentIntent.tryParseUrl('https://www.quantus.com/pay?to=recipient&amount='), isNull); + }); + + test('returns null for a non-pay path', () { + expect(PaymentIntent.tryParseUrl('https://www.quantus.com/account?id=abc'), isNull); + }); + + test('returns null for a malformed url', () { + expect(PaymentIntent.tryParseUrl(':::'), isNull); + }); + }); +} diff --git a/mobile-app/test/unit/shared_address_action_sheet_test.dart b/mobile-app/test/unit/shared_address_action_sheet_test.dart new file mode 100644 index 000000000..848fa42ad --- /dev/null +++ b/mobile-app/test/unit/shared_address_action_sheet_test.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/features/components/shared_address_action_sheet.dart'; +import 'package:resonance_network_wallet/v2/screens/send/input_amount_screen.dart'; +import 'package:resonance_network_wallet/v2/theme/app_theme.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + await SettingsService().initialize(); + }); + + Future pumpSheet(WidgetTester tester, String address) async { + await tester.pumpWidget( + ProviderScope( + child: MediaQuery( + data: const MediaQueryData(size: Size(800, 600)), + child: Builder( + builder: (context) => MaterialApp( + theme: AppTheme.darkTheme(context), + home: Scaffold(body: SharedAddressActionSheet(address: address)), + ), + ), + ), + ), + ); + await tester.pump(); + } + + testWidgets('Send To This Account does not navigate for an invalid address', (tester) async { + await pumpSheet(tester, 'not-a-valid-address'); + + await tester.tap(find.text('Send To This Account')); + await tester.pump(); + + expect(find.byType(InputAmountScreen), findsNothing); + expect(find.text('Invalid address'), findsOneWidget); + + // Let the error toast (10s duration) dismiss so no ticker leaks. + await tester.pump(const Duration(seconds: 11)); + }); +} diff --git a/quantus_sdk/lib/src/services/human_readable_checksum_service.dart b/quantus_sdk/lib/src/services/human_readable_checksum_service.dart index a1d877f60..3f3366e73 100644 --- a/quantus_sdk/lib/src/services/human_readable_checksum_service.dart +++ b/quantus_sdk/lib/src/services/human_readable_checksum_service.dart @@ -50,6 +50,10 @@ class HumanReadableChecksumService { debugPrint('Initialization error stack: $s'); if (!(_isolateReadyCompleter?.isCompleted ?? false)) { _isolateReadyCompleter!.completeError(e); + // The error is already logged and rethrown; keep the completer's + // future from surfacing as an unhandled async error when no + // concurrent caller is awaiting it. + _isolateReadyCompleter!.future.ignore(); } _isolate?.kill(); _isolate = null; diff --git a/quantus_sdk/test/human_readable_checksum_service_test.dart b/quantus_sdk/test/human_readable_checksum_service_test.dart new file mode 100644 index 000000000..964243bef --- /dev/null +++ b/quantus_sdk/test/human_readable_checksum_service_test.dart @@ -0,0 +1,18 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; + +// The checkphrase wordlist asset is bundled with the apps, not with +// quantus_sdk, so loading it here fails — which exercises the failure path. +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('HumanReadableChecksumService.getHumanReadableName failure handling', () { + test('returns null instead of throwing or an empty string when initialization fails', () async { + final name = await HumanReadableChecksumService().getHumanReadableName( + 'qzpyxSr48YN9EQe2ito734iCReTXjnungmNCSY4Yph1YznEda', + ); + + expect(name, isNull); + }); + }); +} From 879267803f891dc13c99e98c404a995bc635581c Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 29 Jul 2026 13:11:16 +0800 Subject: [PATCH 3/3] fix: handle nullable checkphrase in cold wallet call detail view --- cold-wallet-app/lib/components/call_detail_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cold-wallet-app/lib/components/call_detail_view.dart b/cold-wallet-app/lib/components/call_detail_view.dart index 79eb7bdab..66e231517 100644 --- a/cold-wallet-app/lib/components/call_detail_view.dart +++ b/cold-wallet-app/lib/components/call_detail_view.dart @@ -9,7 +9,7 @@ import 'package:quantus_cold_wallet/theme/app_text_styles.dart'; /// destination — an approval or a governance call can name several accounts, and /// each one needs to be verifiable by eye. final _checkphraseProvider = FutureProvider.family((ref, address) async { - return HumanReadableChecksumService().getHumanReadableName(address); + return (await HumanReadableChecksumService().getHumanReadableName(address)) ?? ''; }); /// Renders every parameter of a decoded call, recursing into nested calls.