Skip to content
Merged
11 changes: 7 additions & 4 deletions assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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."
}
"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"
}
74 changes: 63 additions & 11 deletions lib/bloc/coins_bloc/coins_repo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
);
}
}
}

Expand Down Expand Up @@ -832,11 +878,17 @@ 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');
if (!isUserCancellation) {
throw Exception("zcoin activaiton failed: $message");
}
},
needsConfiguration: (coinId, requiredSettings) {
_log.severe(
Expand Down
3 changes: 3 additions & 0 deletions lib/generated/codegen_loader.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

}
2 changes: 1 addition & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Future<void> 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,
Expand Down
66 changes: 65 additions & 1 deletion lib/services/arrr_activation/arrr_activation_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -448,6 +451,67 @@ 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
Future<void> 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 {
// 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
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);
} catch (e, stackTrace) {
_log.severe(
'Failed to update ZHTLC configuration for ${asset.id.id}',
e,
stackTrace,
);
await _cacheActivationError(asset.id, e.toString());
}
}

/// Disable a coin by calling the MM2 disable_coin RPC
/// Copied from CoinsRepo._disableCoin for consistency
Future<void> _disableCoin(String coinId) async {
try {
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
}
}

/// Dispose resources
void dispose() {
// Mark as disposing to prevent new operations
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import 'dart:async';

import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
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({
Expand Down Expand Up @@ -127,6 +134,10 @@ class CoinDetailsCommonButtonsMobileLayout extends StatelessWidget {
),
],
),
if (coin.id.subClass == CoinSubClass.zhtlc) ...[
const SizedBox(height: 12),
ZhtlcConfigButton(coin: coin, isMobile: isMobile),
],
],
);
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -430,3 +447,79 @@ 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<void> _handleConfigUpdate(BuildContext context) async {
final sdk = RepositoryProvider.of<KomodoDefiSdk>(context);
final arrrService = RepositoryProvider.of<ArrrActivationService>(context);
final coinsBloc = context.read<CoinsBloc>();

// 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;
}

ZhtlcUserConfig? newConfig;
try {
newConfig = await confirmZhtlcConfiguration(context, asset: asset);
if (newConfig != null && context.mounted) {
coinsBloc.add(CoinsDeactivated({coin.id.id}));
await 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) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error updating configuration: $e'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
} finally {
if (newConfig != null) {
coinsBloc.add(CoinsActivated([asset.id.id]));
}
}
}

@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: const 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(),
);
}
}
Loading
Loading