From b931995af435c197667ad4c308def28ca45638da Mon Sep 17 00:00:00 2001 From: Francois Date: Tue, 14 Oct 2025 01:55:42 +0200 Subject: [PATCH 1/8] fix(zcoin): show zcoin on reactivation zcoin activation function simply discarded coins that were already active without notifying listeners to display the coin in UI --- lib/bloc/coins_bloc/coins_repo.dart | 64 ++++++++++++++++--- .../zhtlc/zhtlc_activation_status_bar.dart | 34 ++++------ 2 files changed, 69 insertions(+), 29 deletions(-) diff --git a/lib/bloc/coins_bloc/coins_repo.dart b/lib/bloc/coins_bloc/coins_repo.dart index d43411083a..916e18b46f 100644 --- a/lib/bloc/coins_bloc/coins_repo.dart +++ b/lib/bloc/coins_bloc/coins_repo.dart @@ -4,7 +4,6 @@ import 'dart:math' show min; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart' show NetworkImage; - import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart' as kdf_rpc; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; @@ -756,15 +755,62 @@ class CoinsRepo { bool notifyListeners = true, bool addToWalletMetadata = true, }) async { - final inactiveAssets = await assets.removeActiveAssets(_kdfSdk); - for (final asset in inactiveAssets) { + final activatedAssets = await _kdfSdk.assets.getActivatedAssets(); + + for (final asset in assets) { final coin = coins.firstWhere((coin) => coin.id == asset.id); - await _activateZhtlcAsset( - asset, - coin, - notifyListeners: notifyListeners, - addToWalletMetadata: addToWalletMetadata, - ); + + // Check if asset is already activated + final isAlreadyActivated = activatedAssets.any((a) => a.id == asset.id); + + if (isAlreadyActivated) { + _log.info( + 'ZHTLC coin ${coin.id} is already activated. Broadcasting active state.', + ); + + // Add to wallet metadata if requested + if (addToWalletMetadata) { + await _addAssetsToWalletMetdata([asset.id]); + } + + // Broadcast active state for already activated assets + if (notifyListeners) { + _broadcastAsset(coin.copyWith(state: CoinState.active)); + if (coin.id.parentId != null) { + final parentCoin = _assetToCoinWithoutAddress( + _kdfSdk.assets.available[coin.id.parentId]!, + ); + _broadcastAsset(parentCoin.copyWith(state: CoinState.active)); + } + } + + // Subscribe to balance updates for already activated assets + _subscribeToBalanceUpdates(asset); + if (coin.id.parentId != null) { + final parentAsset = _kdfSdk.assets.available[coin.id.parentId]; + if (parentAsset == null) { + _log.warning('Parent asset not found: ${coin.id.parentId}'); + } else { + _subscribeToBalanceUpdates(parentAsset); + } + } + + // Register custom icon if available + if (coin.logoImageUrl?.isNotEmpty ?? false) { + AssetIcon.registerCustomIcon( + coin.id, + NetworkImage(coin.logoImageUrl!), + ); + } + } else { + // Asset needs activation + await _activateZhtlcAsset( + asset, + coin, + notifyListeners: notifyListeners, + addToWalletMetadata: addToWalletMetadata, + ); + } } } diff --git a/lib/views/wallet/wallet_page/common/zhtlc/zhtlc_activation_status_bar.dart b/lib/views/wallet/wallet_page/common/zhtlc/zhtlc_activation_status_bar.dart index 605bab4f7b..a88e70d342 100644 --- a/lib/views/wallet/wallet_page/common/zhtlc/zhtlc_activation_status_bar.dart +++ b/lib/views/wallet/wallet_page/common/zhtlc/zhtlc_activation_status_bar.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart' show ActivationStep, AssetId; +import 'package:komodo_ui/komodo_ui.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart' show LocaleKeys; @@ -91,7 +92,8 @@ class _ZhtlcActivationStatusBarState extends State { @override Widget build(BuildContext context) { - // Filter out completed or error statuses older than 5 seconds + // Filter out completed statuses older than 5 seconds + // Keep error statuses for 60 seconds (these are final errors after all retries) final activeStatuses = _cachedStatuses.entries.where((entry) { final status = entry.value; return status.when( @@ -106,7 +108,7 @@ class _ZhtlcActivationStatusBarState extends State { completed: (coinId, completionTime) => DateTime.now().difference(completionTime).inSeconds < 5, error: (coinId, errorMessage, errorTime) => - DateTime.now().difference(errorTime).inSeconds < 5, + DateTime.now().difference(errorTime).inSeconds < 60, ); }).toList(); @@ -175,24 +177,16 @@ class _ZhtlcActivationStatusBarState extends State { padding: const EdgeInsets.only(top: 4.0), child: status.when( completed: (_, __) => const SizedBox.shrink(), - error: (assetId, errorMessage, errorTime) => Row( - children: [ - Icon( - Icons.error_outline, - size: 14, - color: Theme.of(context).colorScheme.error, - ), - const SizedBox(width: 12), - Expanded( - child: AutoScrollText( - text: errorMessage, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: Theme.of(context).colorScheme.error, - ), - ), - ), - ], + error: (assetId, errorMessage, errorTime) => Padding( + padding: const EdgeInsets.only(top: 8.0), + child: ErrorDisplay( + message: + '${assetId.id}: ${LocaleKeys.activationFailedMessage.tr()}', + detailedMessage: errorMessage, + showDetails: false, + showIcon: true, + narrowBreakpoint: 400, + ), ), inProgress: ( From fcbfa9cf4980851bbdb195f54c081a52227c4e92 Mon Sep 17 00:00:00 2001 From: Francois Date: Tue, 14 Oct 2025 16:52:42 +0200 Subject: [PATCH 2/8] style(zhtlc): move block size and scan interval to advanced section --- assets/translations/en.json | 7 +- lib/generated/codegen_loader.g.dart | 3 + .../zhtlc/zhtlc_configuration_dialog.dart | 162 ++++++++++++++---- 3 files changed, 134 insertions(+), 38 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index d73ae0e965..2be723342a 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -790,5 +790,8 @@ "one": "Activating ZHTLC coin. Please do not close the app or tab until complete.", "other": "Activating ZHTLC coins. Please do not close the app or tab until complete." }, - "zhtlcActivationWarning": "This may take from a few minutes to a few hours, depending on your sync params and how long since your last sync." -} \ No newline at end of file + "zhtlcActivationWarning": "This may take from a few minutes to a few hours, depending on your sync params and how long since your last sync.", + "zhtlcAdvancedConfiguration": "Advanced configuration", + "zhtlcAdvancedConfigurationHint": "Faster intervals (lower milliseconds) and higher blocks per iteration result in higher memory and CPU usage.", + "zhtlcConfigButton": "Config" +} diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index a34c75bce0..dd0c6dfc6b 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -778,5 +778,8 @@ abstract class LocaleKeys { static const zhtlcDateSyncHint = 'zhtlcDateSyncHint'; static const zhtlcActivating = 'zhtlcActivating'; static const zhtlcActivationWarning = 'zhtlcActivationWarning'; + static const zhtlcAdvancedConfiguration = 'zhtlcAdvancedConfiguration'; + static const zhtlcAdvancedConfigurationHint = 'zhtlcAdvancedConfigurationHint'; + static const zhtlcConfigButton = 'zhtlcConfigButton'; } diff --git a/lib/views/wallet/wallet_page/common/zhtlc/zhtlc_configuration_dialog.dart b/lib/views/wallet/wallet_page/common/zhtlc/zhtlc_configuration_dialog.dart index 881c3e0f4f..c0a8dc207c 100644 --- a/lib/views/wallet/wallet_page/common/zhtlc/zhtlc_configuration_dialog.dart +++ b/lib/views/wallet/wallet_page/common/zhtlc/zhtlc_configuration_dialog.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:app_theme/app_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -16,8 +15,8 @@ import 'package:komodo_defi_sdk/komodo_defi_sdk.dart' DownloadResultSuccess; import 'package:komodo_defi_types/komodo_defi_types.dart' show Asset; import 'package:komodo_defi_types/komodo_defi_types.dart'; -import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; enum ZhtlcSyncType { earliest, height, date } @@ -95,6 +94,7 @@ class _ZhtlcConfigurationDialogState extends State { late final TextEditingController intervalMsController; StreamSubscription? _authSubscription; bool _dismissedDueToAuthChange = false; + bool _showAdvancedConfig = false; final GlobalKey<_SyncFormState> _syncFormKey = GlobalKey<_SyncFormState>(); @@ -156,42 +156,40 @@ class _ZhtlcConfigurationDialogState extends State { title: Text( LocaleKeys.zhtlcConfigureTitle.tr(args: [widget.asset.id.id]), ), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!kIsWeb) ...[ - TextField( - controller: zcashPathController, - readOnly: widget.prefilledZcashPath != null, - decoration: InputDecoration( - labelText: LocaleKeys.zhtlcZcashParamsPathLabel.tr(), - helperText: widget.prefilledZcashPath != null - ? LocaleKeys.zhtlcPathAutomaticallyDetected.tr() - : LocaleKeys.zhtlcSaplingParamsFolder.tr(), + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 700, minWidth: 300), + child: IntrinsicWidth( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!kIsWeb) ...[ + TextField( + controller: zcashPathController, + readOnly: widget.prefilledZcashPath != null, + decoration: InputDecoration( + labelText: LocaleKeys.zhtlcZcashParamsPathLabel.tr(), + helperText: widget.prefilledZcashPath != null + ? LocaleKeys.zhtlcPathAutomaticallyDetected.tr() + : LocaleKeys.zhtlcSaplingParamsFolder.tr(), + ), + ), + const SizedBox(height: 12), + ], + _SyncForm(key: _syncFormKey), + const SizedBox(height: 24), + _AdvancedConfigurationSection( + showAdvancedConfig: _showAdvancedConfig, + onToggle: () => setState( + () => _showAdvancedConfig = !_showAdvancedConfig, + ), + blocksPerIterController: blocksPerIterController, + intervalMsController: intervalMsController, ), - ), - const SizedBox(height: 12), - ], - TextField( - controller: blocksPerIterController, - decoration: InputDecoration( - labelText: LocaleKeys.zhtlcBlocksPerIterationLabel.tr(), - ), - keyboardType: TextInputType.number, - ), - const SizedBox(height: 12), - TextField( - controller: intervalMsController, - decoration: InputDecoration( - labelText: LocaleKeys.zhtlcScanIntervalLabel.tr(), - ), - keyboardType: TextInputType.number, + ], ), - const SizedBox(height: 12), - _SyncForm(key: _syncFormKey), - ], + ), ), ), actions: [ @@ -222,6 +220,98 @@ class _ZhtlcConfigurationDialogState extends State { } } +class _AdvancedConfigurationSection extends StatelessWidget { + const _AdvancedConfigurationSection({ + required this.showAdvancedConfig, + required this.onToggle, + required this.blocksPerIterController, + required this.intervalMsController, + }); + + final bool showAdvancedConfig; + final VoidCallback onToggle; + final TextEditingController blocksPerIterController; + final TextEditingController intervalMsController; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: onToggle, + child: Row( + children: [ + Icon(showAdvancedConfig ? Icons.expand_less : Icons.expand_more), + const SizedBox(width: 8), + Text( + LocaleKeys.zhtlcAdvancedConfiguration.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + ), + if (showAdvancedConfig) ...[ + const SizedBox(height: 12), + const _AdvancedConfigurationWarning(), + const SizedBox(height: 12), + TextField( + controller: blocksPerIterController, + decoration: InputDecoration( + labelText: LocaleKeys.zhtlcBlocksPerIterationLabel.tr(), + ), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 12), + TextField( + controller: intervalMsController, + decoration: InputDecoration( + labelText: LocaleKeys.zhtlcScanIntervalLabel.tr(), + ), + keyboardType: TextInputType.number, + ), + ], + ], + ); + } +} + +class _AdvancedConfigurationWarning extends StatelessWidget { + const _AdvancedConfigurationWarning(); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final backgroundColor = theme.colorScheme.secondaryContainer; + final foregroundColor = theme.colorScheme.onSecondaryContainer; + + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: backgroundColor.withValues(alpha: 0.1), + border: Border.all(color: foregroundColor), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.info_outline, color: foregroundColor), + const SizedBox(width: 12), + Expanded( + child: Text( + LocaleKeys.zhtlcAdvancedConfigurationHint.tr(), + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: foregroundColor), + softWrap: true, + ), + ), + ], + ), + ); + } +} + class _SyncForm extends StatefulWidget { const _SyncForm({super.key}); From a703e00ea9e6946fce76a541247ad4a2b7ddc8b1 Mon Sep 17 00:00:00 2001 From: Francois Date: Wed, 15 Oct 2025 00:34:10 +0200 Subject: [PATCH 3/8] feat(zhtlc): add config modification option to coin details page --- assets/translations/en.json | 2 +- lib/main.dart | 2 +- .../arrr_activation_service.dart | 79 ++++++++++++++- .../coin_details_common_buttons.dart | 95 ++++++++++++++++++- 4 files changed, 173 insertions(+), 5 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index 2be723342a..20fffd2154 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -793,5 +793,5 @@ "zhtlcActivationWarning": "This may take from a few minutes to a few hours, depending on your sync params and how long since your last sync.", "zhtlcAdvancedConfiguration": "Advanced configuration", "zhtlcAdvancedConfigurationHint": "Faster intervals (lower milliseconds) and higher blocks per iteration result in higher memory and CPU usage.", - "zhtlcConfigButton": "Config" + "zhtlcConfigButton": "Modify sync config" } diff --git a/lib/main.dart b/lib/main.dart index 392f3e3ae1..894449396c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -74,7 +74,7 @@ Future main() async { final tradingStatusRepository = TradingStatusRepository(komodoDefiSdk); final tradingStatusService = TradingStatusService(tradingStatusRepository); await tradingStatusService.initialize(); - final arrrActivationService = ArrrActivationService(komodoDefiSdk); + final arrrActivationService = ArrrActivationService(komodoDefiSdk, mm2); final coinsRepo = CoinsRepo( kdfSdk: komodoDefiSdk, diff --git a/lib/services/arrr_activation/arrr_activation_service.dart b/lib/services/arrr_activation/arrr_activation_service.dart index a843b14011..d7f8bffee3 100644 --- a/lib/services/arrr_activation/arrr_activation_service.dart +++ b/lib/services/arrr_activation/arrr_activation_service.dart @@ -6,18 +6,21 @@ import 'package:komodo_defi_types/komodo_defi_type_utils.dart' import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:logging/logging.dart'; import 'package:mutex/mutex.dart'; +import 'package:web_dex/mm2/mm2.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/disable_coin/disable_coin_req.dart'; import 'arrr_config.dart'; /// Service layer - business logic coordination for ARRR activation class ArrrActivationService { - ArrrActivationService(this._sdk) + ArrrActivationService(this._sdk, this._mm2) : _configService = _sdk.activationConfigService { _startListeningToAuthChanges(); } final ActivationConfigService _configService; final KomodoDefiSdk _sdk; + final MM2 _mm2; final Logger _log = Logger('ArrrActivationService'); /// Stream controller for configuration requests @@ -448,6 +451,80 @@ class ArrrActivationService { ); } + /// Updates the configuration for an already activated ZHTLC coin + /// This will: + /// 1. Cancel any ongoing activation tasks for the asset + /// 2. Disable the coin if it's currently active + /// 3. Store the new configuration + /// 4. Reactivate the coin with the updated configuration + Future updateZhtlcConfig( + Asset asset, + ZhtlcUserConfig newConfig, + ) async { + if (_isDisposing || _configRequestController.isClosed) { + throw StateError('ArrrActivationService has been disposed'); + } + + _log.info('Updating ZHTLC configuration for ${asset.id.id}'); + + try { + // 1. Cancel any ongoing activation for this asset + final ongoingActivation = _ongoingActivations[asset.id]; + if (ongoingActivation != null) { + _log.info('Cancelling ongoing activation for ${asset.id.id}'); + _ongoingActivations.remove(asset.id); + // Note: We can't actually cancel the future, but we remove it from tracking + } + + // Cancel any pending configuration requests + final completer = _configCompleters[asset.id]; + if (completer != null && !completer.isCompleted) { + _log.info( + 'Cancelling pending configuration request for ${asset.id.id}', + ); + completer.complete(null); + _configCompleters.remove(asset.id); + } + + // 2. Disable the coin if it's currently active + final activatedAssets = await _sdk.assets.getActivatedAssets(); + final isCurrentlyActive = activatedAssets.any((a) => a.id == asset.id); + + if (isCurrentlyActive) { + _log.info('Disabling currently active ZHTLC coin ${asset.id.id}'); + await _disableCoin(asset.id.id); + } + + // 3. Store the new configuration + _log.info('Saving new configuration for ${asset.id.id}'); + await _configService.saveZhtlcConfig(asset.id, newConfig); + + // 4. Reactivate the coin with the updated configuration + _log.info('Reactivating ${asset.id.id} with updated configuration'); + return await activateArrr(asset, initialConfig: newConfig); + } catch (e, stackTrace) { + _log.severe( + 'Failed to update ZHTLC configuration for ${asset.id.id}', + e, + stackTrace, + ); + await _cacheActivationError(asset.id, e.toString()); + return ArrrActivationResultError('Failed to update configuration: $e'); + } + } + + /// Disable a coin by calling the MM2 disable_coin RPC + /// Copied from CoinsRepo._disableCoin for consistency + Future _disableCoin(String coinId) async { + try { + await _mm2.call(DisableCoinReq(coin: coinId)); + _log.info('Successfully disabled coin $coinId'); + } catch (e, s) { + _log.shout('Error disabling $coinId', e, s); + // Don't rethrow - we want to continue with the configuration update + } + } + /// Dispose resources void dispose() { // Mark as disposing to prevent new operations diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart index 4b5f2d3954..947be786e6 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -5,18 +7,23 @@ import 'package:flutter_svg/svg.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:komodo_ui/komodo_ui.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_bloc.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; +import 'package:web_dex/model/main_menu_value.dart'; import 'package:web_dex/model/wallet.dart'; -import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/router/state/routing_state.dart'; +import 'package:web_dex/services/arrr_activation/arrr_activation_service.dart'; import 'package:web_dex/views/bitrefill/bitrefill_button.dart'; import 'package:web_dex/views/wallet/coin_details/coin_details_info/coin_addresses.dart'; import 'package:web_dex/views/wallet/coin_details/coin_details_info/contract_address_button.dart'; import 'package:web_dex/views/wallet/coin_details/coin_page_type.dart'; +import 'package:web_dex/views/wallet/wallet_page/common/zhtlc/zhtlc_configuration_dialog.dart'; class CoinDetailsCommonButtons extends StatelessWidget { const CoinDetailsCommonButtons({ @@ -127,6 +134,10 @@ class CoinDetailsCommonButtonsMobileLayout extends StatelessWidget { ), ], ), + if (coin.id.subClass == CoinSubClass.zhtlc) ...[ + const SizedBox(height: 12), + ZhtlcConfigButton(coin: coin, isMobile: isMobile), + ], ], ); } @@ -199,6 +210,12 @@ class CoinDetailsCommonButtonsDesktopLayout extends StatelessWidget { tooltip: _getBitrefillTooltip(coin), ), ), + if (coin.id.subClass == CoinSubClass.zhtlc) + Container( + margin: const EdgeInsets.only(left: 21), + constraints: const BoxConstraints(maxWidth: 120), + child: ZhtlcConfigButton(coin: coin, isMobile: isMobile), + ), Flexible( flex: 2, child: Align( @@ -430,3 +447,77 @@ String? _getBitrefillTooltip(Coin coin) { // Check if coin has zero balance (this could be enhanced with actual balance check) return null; // Let BitrefillButton handle the zero balance tooltip } + +class ZhtlcConfigButton extends StatelessWidget { + const ZhtlcConfigButton({ + required this.coin, + required this.isMobile, + super.key, + }); + + final Coin coin; + final bool isMobile; + + Future _handleConfigUpdate(BuildContext context) async { + final sdk = RepositoryProvider.of(context); + final arrrService = RepositoryProvider.of(context); + final coinsBloc = context.read(); + + // Get the asset from the SDK + final asset = sdk.assets.available[coin.id]; + if (asset == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Asset ${coin.id.id} not found'), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + return; + } + + try { + // Show the configuration dialog + final newConfig = await confirmZhtlcConfiguration(context, asset: asset); + + if (newConfig != null && context.mounted) { + // Deactivate the coin first using CoinsBloc + coinsBloc.add(CoinsDeactivated({coin.id.id})); + + // Navigate to main wallet page + routingState.selectedMenu = MainMenuValue.defaultMenu(); + + // Update the configuration (will trigger reactivation) + unawaited(arrrService.updateZhtlcConfig(asset, newConfig)); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error updating configuration: $e'), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + final ThemeData themeData = Theme.of(context); + return UiPrimaryButton( + key: const Key('coin-details-zhtlc-config-button'), + height: isMobile ? 52 : 40, + prefix: Container( + padding: const EdgeInsets.only(right: 14), + child: Icon(Icons.settings, size: 18), + ), + textStyle: themeData.textTheme.labelLarge?.copyWith( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + backgroundColor: themeData.colorScheme.tertiary, + onPressed: coin.isActive ? () => _handleConfigUpdate(context) : null, + text: LocaleKeys.zhtlcConfigButton.tr(), + ); + } +} From 1dcf5f9c1cae1e32afc45b4f48178e15eefafcc9 Mon Sep 17 00:00:00 2001 From: Francois Date: Wed, 15 Oct 2025 01:12:51 +0200 Subject: [PATCH 4/8] fix(zhtlc): simplify the config update and reactivation --- assets/translations/en.json | 2 +- .../arrr_activation_service.dart | 26 ++++++++----------- .../coin_details_common_buttons.dart | 14 +++++----- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index 20fffd2154..2be723342a 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -793,5 +793,5 @@ "zhtlcActivationWarning": "This may take from a few minutes to a few hours, depending on your sync params and how long since your last sync.", "zhtlcAdvancedConfiguration": "Advanced configuration", "zhtlcAdvancedConfigurationHint": "Faster intervals (lower milliseconds) and higher blocks per iteration result in higher memory and CPU usage.", - "zhtlcConfigButton": "Modify sync config" + "zhtlcConfigButton": "Config" } diff --git a/lib/services/arrr_activation/arrr_activation_service.dart b/lib/services/arrr_activation/arrr_activation_service.dart index d7f8bffee3..64e1223fe9 100644 --- a/lib/services/arrr_activation/arrr_activation_service.dart +++ b/lib/services/arrr_activation/arrr_activation_service.dart @@ -457,7 +457,7 @@ class ArrrActivationService { /// 2. Disable the coin if it's currently active /// 3. Store the new configuration /// 4. Reactivate the coin with the updated configuration - Future updateZhtlcConfig( + Future updateZhtlcConfig( Asset asset, ZhtlcUserConfig newConfig, ) async { @@ -487,21 +487,11 @@ class ArrrActivationService { } // 2. Disable the coin if it's currently active - final activatedAssets = await _sdk.assets.getActivatedAssets(); - final isCurrentlyActive = activatedAssets.any((a) => a.id == asset.id); - - if (isCurrentlyActive) { - _log.info('Disabling currently active ZHTLC coin ${asset.id.id}'); - await _disableCoin(asset.id.id); - } + await _disableCoin(asset.id.id); // 3. Store the new configuration _log.info('Saving new configuration for ${asset.id.id}'); await _configService.saveZhtlcConfig(asset.id, newConfig); - - // 4. Reactivate the coin with the updated configuration - _log.info('Reactivating ${asset.id.id} with updated configuration'); - return await activateArrr(asset, initialConfig: newConfig); } catch (e, stackTrace) { _log.severe( 'Failed to update ZHTLC configuration for ${asset.id.id}', @@ -509,7 +499,6 @@ class ArrrActivationService { stackTrace, ); await _cacheActivationError(asset.id, e.toString()); - return ArrrActivationResultError('Failed to update configuration: $e'); } } @@ -517,8 +506,15 @@ class ArrrActivationService { /// Copied from CoinsRepo._disableCoin for consistency Future _disableCoin(String coinId) async { try { - await _mm2.call(DisableCoinReq(coin: coinId)); - _log.info('Successfully disabled coin $coinId'); + final activatedAssets = await _sdk.assets.getEnabledCoins(); + final isCurrentlyActive = activatedAssets.any( + (configId) => configId == coinId, + ); + if (isCurrentlyActive) { + _log.info('Disabling currently active ZHTLC coin $coinId'); + await _mm2.call(DisableCoinReq(coin: coinId)); + _log.info('Successfully disabled coin $coinId'); + } } catch (e, s) { _log.shout('Error disabling $coinId', e, s); // Don't rethrow - we want to continue with the configuration update diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart index 947be786e6..b21252c5b7 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart @@ -476,18 +476,16 @@ class ZhtlcConfigButton extends StatelessWidget { } try { - // Show the configuration dialog final newConfig = await confirmZhtlcConfiguration(context, asset: asset); - if (newConfig != null && context.mounted) { - // Deactivate the coin first using CoinsBloc coinsBloc.add(CoinsDeactivated({coin.id.id})); + await arrrService.updateZhtlcConfig(asset, newConfig); + coinsBloc.add(CoinsActivated([asset.id.id])); - // Navigate to main wallet page - routingState.selectedMenu = MainMenuValue.defaultMenu(); - - // Update the configuration (will trigger reactivation) - unawaited(arrrService.updateZhtlcConfig(asset, newConfig)); + // Forcefully navigate back to wallet page so that the zhtlc status bar + // is visible, rather than allowing periodic balance, pubkey, and tx + // history requests to continue running and failing during activation + routingState.selectedMenu = MainMenuValue.wallet; } } catch (e) { if (context.mounted) { From 04e4e0f0e1ec01e3cd2c6c1dfdd79143884c3be5 Mon Sep 17 00:00:00 2001 From: Francois Date: Wed, 15 Oct 2025 01:14:52 +0200 Subject: [PATCH 5/8] chore(sdk): update SDK to 448eecc to switch to KDF staging branch --- .gitignore | 1 + sdk | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c55afa38a5..754e0ef05a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +.kiro/ # IntelliJ related *.iml diff --git a/sdk b/sdk index 679fd92a6c..448eecc0dc 160000 --- a/sdk +++ b/sdk @@ -1 +1 @@ -Subproject commit 679fd92a6ce6d87b631dadbcaca1c8343e6580c2 +Subproject commit 448eecc0dcd88473edb227dd0e47bf2cc6001a09 From 30b38daaff09eefd710aaf0dc9d4dcc997b988c1 Mon Sep 17 00:00:00 2001 From: Francois Date: Wed, 15 Oct 2025 01:20:27 +0200 Subject: [PATCH 6/8] chore(zhtlc): reword dialog title and apply suggestions --- assets/translations/en.json | 4 ++-- .../arrr_activation/arrr_activation_service.dart | 9 --------- .../coin_details_info/coin_details_common_buttons.dart | 6 ++++-- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index 2be723342a..d83fa5e865 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -767,13 +767,13 @@ "fetchingPrivateKeysMessage": "Please wait while we securely fetch your private keys...", "pubkeyType": "Type", "securitySettings": "Security Settings", - "zhtlcConfigureTitle": "Configure {}", + "zhtlcConfigureTitle": "Configure {} chain sync", "zhtlcZcashParamsPathLabel": "Zcash parameters path", "zhtlcPathAutomaticallyDetected": "Path automatically detected", "zhtlcSaplingParamsFolder": "Folder containing sapling params", "zhtlcBlocksPerIterationLabel": "Blocks per iteration", "zhtlcScanIntervalLabel": "Scan interval (ms)", - "zhtlcStartSyncFromLabel": "Start sync from:", + "zhtlcStartSyncFromLabel": "Start chain sync from:", "zhtlcEarliestSaplingOption": "Earliest (sapling)", "zhtlcBlockHeightOption": "Block height", "zhtlcShieldedAddress": "Shielded", diff --git a/lib/services/arrr_activation/arrr_activation_service.dart b/lib/services/arrr_activation/arrr_activation_service.dart index 64e1223fe9..80be34869e 100644 --- a/lib/services/arrr_activation/arrr_activation_service.dart +++ b/lib/services/arrr_activation/arrr_activation_service.dart @@ -456,7 +456,6 @@ class ArrrActivationService { /// 1. Cancel any ongoing activation tasks for the asset /// 2. Disable the coin if it's currently active /// 3. Store the new configuration - /// 4. Reactivate the coin with the updated configuration Future updateZhtlcConfig( Asset asset, ZhtlcUserConfig newConfig, @@ -468,14 +467,6 @@ class ArrrActivationService { _log.info('Updating ZHTLC configuration for ${asset.id.id}'); try { - // 1. Cancel any ongoing activation for this asset - final ongoingActivation = _ongoingActivations[asset.id]; - if (ongoingActivation != null) { - _log.info('Cancelling ongoing activation for ${asset.id.id}'); - _ongoingActivations.remove(asset.id); - // Note: We can't actually cancel the future, but we remove it from tracking - } - // Cancel any pending configuration requests final completer = _configCompleters[asset.id]; if (completer != null && !completer.isCompleted) { diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart index b21252c5b7..d49a20e654 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart @@ -480,7 +480,6 @@ class ZhtlcConfigButton extends StatelessWidget { if (newConfig != null && context.mounted) { coinsBloc.add(CoinsDeactivated({coin.id.id})); await arrrService.updateZhtlcConfig(asset, newConfig); - coinsBloc.add(CoinsActivated([asset.id.id])); // Forcefully navigate back to wallet page so that the zhtlc status bar // is visible, rather than allowing periodic balance, pubkey, and tx @@ -496,6 +495,9 @@ class ZhtlcConfigButton extends StatelessWidget { ), ); } + } finally { + // Ensure that the coin is reactivated regardless of success of update + coinsBloc.add(CoinsActivated([asset.id.id])); } } @@ -507,7 +509,7 @@ class ZhtlcConfigButton extends StatelessWidget { height: isMobile ? 52 : 40, prefix: Container( padding: const EdgeInsets.only(right: 14), - child: Icon(Icons.settings, size: 18), + child: const Icon(Icons.settings, size: 18), ), textStyle: themeData.textTheme.labelLarge?.copyWith( fontSize: 14, From 7392f0ff812ba3d21e145c3141f65fa843311c18 Mon Sep 17 00:00:00 2001 From: Francois Date: Wed, 15 Oct 2025 02:23:25 +0200 Subject: [PATCH 7/8] fix(zhtlc): coin incorrectly added to list on dialog cancel --- lib/bloc/coins_bloc/coins_repo.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/bloc/coins_bloc/coins_repo.dart b/lib/bloc/coins_bloc/coins_repo.dart index 916e18b46f..48bd68ca51 100644 --- a/lib/bloc/coins_bloc/coins_repo.dart +++ b/lib/bloc/coins_bloc/coins_repo.dart @@ -878,11 +878,13 @@ class CoinsRepo { 'ZHTLC asset activation failed: ${asset.id.id} - $message', ); - if (notifyListeners) { + // Only broadcast suspended state if it's not a user cancellation + // User cancellations have the message "Configuration cancelled by user or timed out" + final isUserCancellation = message.contains('cancelled by user'); + + if (notifyListeners && !isUserCancellation) { _broadcastAsset(coin.copyWith(state: CoinState.suspended)); } - - throw Exception('ZHTLC activation failed: $message'); }, needsConfiguration: (coinId, requiredSettings) { _log.severe( From 77ac0c7da4c78113153b20fc03bafcc2a2105229 Mon Sep 17 00:00:00 2001 From: Francois Date: Thu, 16 Oct 2025 17:26:19 +0200 Subject: [PATCH 8/8] fix(zcoin): rethrow if user action is not mismatch of assumptions: activateArrr returns an activation error result rather than throwing an exception, so it has to be rethrown in the error handler for it to bubble up. there is also a mismatch between single and multiple activations, but that is fine for now since single activations are all that are present --- lib/bloc/coins_bloc/coins_repo.dart | 4 ++++ .../coin_details_info/coin_details_common_buttons.dart | 8 +++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/bloc/coins_bloc/coins_repo.dart b/lib/bloc/coins_bloc/coins_repo.dart index 48bd68ca51..cf5fb37f94 100644 --- a/lib/bloc/coins_bloc/coins_repo.dart +++ b/lib/bloc/coins_bloc/coins_repo.dart @@ -885,6 +885,10 @@ class CoinsRepo { if (notifyListeners && !isUserCancellation) { _broadcastAsset(coin.copyWith(state: CoinState.suspended)); } + + if (!isUserCancellation) { + throw Exception("zcoin activaiton failed: $message"); + } }, needsConfiguration: (coinId, requiredSettings) { _log.severe( diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart index d49a20e654..1df71d84c1 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart @@ -475,8 +475,9 @@ class ZhtlcConfigButton extends StatelessWidget { return; } + ZhtlcUserConfig? newConfig; try { - final newConfig = await confirmZhtlcConfiguration(context, asset: asset); + newConfig = await confirmZhtlcConfiguration(context, asset: asset); if (newConfig != null && context.mounted) { coinsBloc.add(CoinsDeactivated({coin.id.id})); await arrrService.updateZhtlcConfig(asset, newConfig); @@ -496,8 +497,9 @@ class ZhtlcConfigButton extends StatelessWidget { ); } } finally { - // Ensure that the coin is reactivated regardless of success of update - coinsBloc.add(CoinsActivated([asset.id.id])); + if (newConfig != null) { + coinsBloc.add(CoinsActivated([asset.id.id])); + } } }