From 8c4f4b43a5bbf58d9a126d180895fa62d9442005 Mon Sep 17 00:00:00 2001 From: Francois Date: Fri, 3 Oct 2025 12:14:03 +0200 Subject: [PATCH 1/2] fix: flush balance, transaction history, and pubkey watchers on auth event --- .../bloc/coin_addresses_bloc.dart | 143 ++++++++++-- .../bloc/coin_addresses_event.dart | 13 +- lib/bloc/coins_bloc/coins_bloc.dart | 4 +- lib/bloc/coins_bloc/coins_repo.dart | 137 ++++++++---- .../transaction_history_bloc.dart | 210 ++++++++++-------- .../transaction_history_event.dart | 5 + 6 files changed, 346 insertions(+), 166 deletions(-) diff --git a/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart b/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart index bf43c9aa80..2ef681047b 100644 --- a/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart +++ b/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart @@ -3,7 +3,8 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart' - show Asset, NewAddressStatus, AssetPubkeys; + show Asset, NewAddressStatus, AssetPubkeys, KdfUser, WalletId; +import 'package:logging/logging.dart'; import 'package:web_dex/analytics/events.dart'; import 'package:web_dex/bloc/analytics/analytics_bloc.dart'; import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_event.dart'; @@ -11,12 +12,7 @@ import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_state.dart'; import 'package:web_dex/bloc/coins_bloc/asset_coin_extension.dart'; class CoinAddressesBloc extends Bloc { - final KomodoDefiSdk sdk; - final String assetId; - final AnalyticsBloc analyticsBloc; - - StreamSubscription? _pubkeysSub; - CoinAddressesBloc(this.sdk, this.assetId, this.analyticsBloc) + CoinAddressesBloc(this._sdk, this._assetId, this._analyticsBloc) : super(const CoinAddressesState()) { on(_onCreateAddressSubmitted); on(_onStarted); @@ -24,8 +20,28 @@ class CoinAddressesBloc extends Bloc { on(_onHideZeroBalanceChanged); on(_onPubkeysUpdated); on(_onPubkeysSubscriptionFailed); + on(_onAuthStateChanged); + + // Listen to auth state changes to clear subscriptions on logout/wallet switch + _authSubscription = _sdk.auth.watchCurrentUser().listen((user) { + if (!isClosed) { + add(CoinAddressesAuthStateChanged(user)); + } + }); } + final KomodoDefiSdk _sdk; + final String _assetId; + final AnalyticsBloc _analyticsBloc; + + static final Logger _log = Logger('CoinAddressesBloc'); + + StreamSubscription? _pubkeysSub; + StreamSubscription? _authSubscription; + + /// Current wallet ID being tracked for auth state changes + WalletId? _currentWalletId; + Future _onStarted( CoinAddressesStarted event, Emitter emit, @@ -44,8 +60,8 @@ class CoinAddressesBloc extends Bloc { ), ); try { - final asset = getSdkAsset(sdk, assetId); - final stream = sdk.pubkeys.watchCreateNewPubkey(asset); + final asset = getSdkAsset(_sdk, _assetId); + final stream = _sdk.pubkeys.watchCreateNewPubkey(asset); await for (final newAddressState in stream) { emit(state.copyWith(newAddressState: () => newAddressState)); @@ -57,11 +73,11 @@ class CoinAddressesBloc extends Bloc { if (derivation != null) { try { final parsed = parseDerivationPath(derivation); - analyticsBloc.logEvent( + _analyticsBloc.logEvent( HdAddressGeneratedEventData( accountIndex: parsed.accountIndex, addressIndex: parsed.addressIndex, - asset: assetId, + asset: _assetId, ), ); } catch (_) { @@ -118,10 +134,10 @@ class CoinAddressesBloc extends Bloc { emit(state.copyWith(status: () => FormStatus.submitting)); try { - final asset = getSdkAsset(sdk, assetId); - final addresses = (await asset.getPubkeys(sdk)).keys; + final asset = getSdkAsset(_sdk, _assetId); + final addresses = (await asset.getPubkeys(_sdk)).keys; - final reasons = await asset.getCantCreateNewAddressReasons(sdk); + final reasons = await asset.getCantCreateNewAddressReasons(_sdk); emit( state.copyWith( @@ -155,8 +171,8 @@ class CoinAddressesBloc extends Bloc { Emitter emit, ) async { try { - final asset = getSdkAsset(sdk, assetId); - final reasons = await asset.getCantCreateNewAddressReasons(sdk); + final asset = getSdkAsset(_sdk, _assetId); + final reasons = await asset.getCantCreateNewAddressReasons(_sdk); emit( state.copyWith( status: () => FormStatus.success, @@ -184,28 +200,44 @@ class CoinAddressesBloc extends Bloc { Future _startWatchingPubkeys(Asset asset) async { try { - await _pubkeysSub?.cancel(); - _pubkeysSub = null; + // Cancel any existing subscription first + await _cancelPubkeySubscription(); + + _log.fine('Starting pubkey watching for asset ${asset.id.id}'); + // Pre-cache pubkeys to ensure that any newly created pubkeys are available // when we start watching. UI flickering between old and new states is // avoided this way. The watchPubkeys function yields the last known pubkeys // when the pubkeys stream is first activated. - await sdk.pubkeys.precachePubkeys(asset); - _pubkeysSub = sdk.pubkeys - .watchPubkeys(asset, activateIfNeeded: true) + await _sdk.pubkeys.precachePubkeys(asset); + _pubkeysSub = _sdk.pubkeys + .watchPubkeys(asset) .listen( (AssetPubkeys assetPubkeys) { if (!isClosed) { + _log.finest( + 'Received pubkey update for asset ${asset.id.id}: ${assetPubkeys.keys.length} addresses', + ); add(CoinAddressesPubkeysUpdated(assetPubkeys.keys)); } }, onError: (Object err) { + _log.warning( + 'Pubkey subscription error for asset ${asset.id.id}: $err', + ); if (!isClosed) { add(CoinAddressesPubkeysSubscriptionFailed(err.toString())); } }, ); + + _log.fine( + 'Pubkey watching started successfully for asset ${asset.id.id}', + ); } catch (e) { + _log.severe( + 'Failed to start pubkey watching for asset ${asset.id.id}: $e', + ); if (!isClosed) { add(CoinAddressesPubkeysSubscriptionFailed(e.toString())); } @@ -214,8 +246,73 @@ class CoinAddressesBloc extends Bloc { @override Future close() async { - await _pubkeysSub?.cancel(); - _pubkeysSub = null; + _log.fine('Closing CoinAddressesBloc for asset $_assetId'); + + // Cancel auth subscription + try { + await _authSubscription?.cancel(); + _authSubscription = null; + } catch (e) { + _log.warning( + 'Error cancelling auth subscription for asset $_assetId: $e', + ); + } + + // Cancel pubkey subscription + await _cancelPubkeySubscription(); + return super.close(); } + + /// Clears pubkeys subscription when auth state changes (logout or wallet switch). + /// This prevents stale subscriptions from continuing to receive updates + /// for the previous wallet's addresses. + Future _onAuthStateChanged( + CoinAddressesAuthStateChanged event, + Emitter emit, + ) async { + final newWalletId = event.user?.walletId; + + _log.fine( + 'Auth state changed for asset $_assetId: ${_currentWalletId?.name} -> ' + '${newWalletId?.name}', + ); + + // If the wallet ID has changed, clear subscriptions and reset state + if (_currentWalletId != newWalletId) { + _log.info( + 'Wallet change detected for asset $_assetId, clearing pubkey subscriptions', + ); + + await _cancelPubkeySubscription(); + _currentWalletId = newWalletId; + + // Reset to initial state when wallet changes + emit(const CoinAddressesState()); + + _log.fine( + 'Auth state change handling completed for asset $_assetId, wallet: ' + '${newWalletId?.name}', + ); + } else { + _log.finest( + 'No wallet change detected for asset $_assetId, keeping current state', + ); + } + } + + /// Cancels the current pubkey subscription with proper error handling + Future _cancelPubkeySubscription() async { + try { + await _pubkeysSub?.cancel(); + _pubkeysSub = null; + _log.fine('Pubkey subscription cancelled for asset $_assetId'); + } catch (e) { + _log.warning( + 'Error cancelling pubkey subscription for asset $_assetId: $e', + ); + // Still set to null to prevent further issues + _pubkeysSub = null; + } + } } diff --git a/lib/bloc/coin_addresses/bloc/coin_addresses_event.dart b/lib/bloc/coin_addresses/bloc/coin_addresses_event.dart index 3fa8979145..e09ef81e61 100644 --- a/lib/bloc/coin_addresses/bloc/coin_addresses_event.dart +++ b/lib/bloc/coin_addresses/bloc/coin_addresses_event.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; -import 'package:komodo_defi_types/komodo_defi_types.dart' show PubkeyInfo; +import 'package:komodo_defi_types/komodo_defi_types.dart' + show PubkeyInfo, KdfUser; abstract class CoinAddressesEvent extends Equatable { const CoinAddressesEvent(); @@ -46,3 +47,13 @@ class CoinAddressesPubkeysSubscriptionFailed extends CoinAddressesEvent { @override List get props => [error]; } + +/// Event triggered when auth state changes (logout/wallet switch) +class CoinAddressesAuthStateChanged extends CoinAddressesEvent { + final KdfUser? user; + + const CoinAddressesAuthStateChanged(this.user); + + @override + List get props => [user]; +} diff --git a/lib/bloc/coins_bloc/coins_bloc.dart b/lib/bloc/coins_bloc/coins_bloc.dart index 928121ea39..b73f541499 100644 --- a/lib/bloc/coins_bloc/coins_bloc.dart +++ b/lib/bloc/coins_bloc/coins_bloc.dart @@ -322,7 +322,7 @@ class CoinsBloc extends Bloc { // Ensure any cached addresses/pubkeys from a previous wallet are cleared // so that UI fetches fresh pubkeys for the newly logged-in wallet. emit(state.copyWith(pubkeys: {})); - _coinsRepo.flushCache(); + await _coinsRepo.flushCache(); final Wallet currentWallet = event.signedInUser.wallet; // Start off by emitting the newly activated coins so that they all appear @@ -369,7 +369,7 @@ class CoinsBloc extends Bloc { pubkeys: {}, ), ); - _coinsRepo.flushCache(); + await _coinsRepo.flushCache(); } void _scheduleInitialBalanceRefresh(Iterable coinsToActivate) { diff --git a/lib/bloc/coins_bloc/coins_repo.dart b/lib/bloc/coins_bloc/coins_repo.dart index d43411083a..d454c30483 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'; @@ -102,7 +101,6 @@ class CoinsRepo { return; } - // Start a new subscription _balanceWatchers[asset.id] = _kdfSdk.balances.watchBalance(asset.id).listen( (balanceInfo) { // Update the balance cache with the new values @@ -114,26 +112,53 @@ class CoinsRepo { ); } - void flushCache() { - // Intentionally avoid flushing the prices cache - prices are independent - // of the user's session and should be updated on a regular basis. - _addressCache.clear(); - _balancesCache.clear(); - - // Cancel all balance watchers - for (final subscription in _balanceWatchers.values) { - subscription.cancel(); + /// Logs activation coordination events with appropriate context for debugging + void _logActivationCoordinationEvent(AssetId assetId, Object error) { + if (error is StateError && + (error.message.contains('activation in progress') || + error.message.contains('already activated'))) { + // These are expected coordination conflicts, log at info level + _log.info( + 'Activation coordination event for ${assetId.id}: ${error.message}', + ); + } else { + // Unexpected errors should be logged at warning level for coordination events + _log.warning('Activation coordination issue for ${assetId.id}: $error'); } - _balanceWatchers.clear(); } - void dispose() { - for (final subscription in _balanceWatchers.values) { - subscription.cancel(); + /// Logs activation errors with appropriate context for debugging + void _logActivationError( + AssetId assetId, + Object error, + StackTrace stackTrace, + ) { + if (error is StateError && + (error.message.contains('activation in progress') || + error.message.contains('already activated'))) { + // These are expected coordination conflicts, log at info level + _log.info( + 'Activation coordination event for ${assetId.id}: ${error.message}', + ); + } else { + // Unexpected errors should be logged at severe level + _log.severe( + 'Error activating asset after retries: ${assetId.id}', + error, + stackTrace, + ); } - _balanceWatchers.clear(); + } + + Future flushCache() async { + // Intentionally avoid flushing the prices cache - prices are independent + // of the user's session and should be updated on a regular basis. + _addressCache.clear(); - enabledAssetsChanges.close(); + _log.info('Clearing ${_balanceWatchers.length} balance watchers'); + final cancelFutures = _balanceWatchers.values.map((sub) => sub.cancel()); + await Future.wait(cancelFutures); + _balanceWatchers.clear(); } /// Returns all known coins, optionally filtering out excluded assets. @@ -320,29 +345,49 @@ class CoinsRepo { for (final asset in assets) { final coin = _assetToCoinWithoutAddress(asset); + + _log.info('Starting activation for asset ${asset.id.id}'); + try { if (notifyListeners) { _broadcastAsset(coin.copyWith(state: CoinState.activating)); } - // Use retry with exponential backoff for activation + // Use retry with exponential backoff for activation with enhanced error handling await retry( () async { - // exception is thrown if the asset is already activated, so manual - // check is needed for now until specific exception type can be caught - final activatedAssets = await _kdfSdk.assets.getActivatedAssets(); - if (activatedAssets.any((a) => a.id == asset.id)) { - _log.info( - 'Coin ${coin.id} is already activated. Skipping activation.', - ); - return; - } - - final progress = await _kdfSdk.assets.activateAsset(asset).last; - if (!progress.isSuccess) { - throw Exception( - progress.errorMessage ?? 'Activation failed for ${asset.id.id}', - ); + try { + // Check if asset is already active to prevent duplicate activation + final activatedAssets = await _kdfSdk.assets.getActivatedAssets(); + if (activatedAssets.any((a) => a.id == asset.id)) { + _log.info( + 'Coin ${coin.id} is already activated. Skipping activation.', + ); + return; + } + + // Use the SDK's activation stream and wait for completion + final progress = await _kdfSdk.assets.activateAsset(asset).last; + if (!progress.isSuccess) { + throw Exception( + progress.errorMessage ?? + 'Activation failed for ${asset.id.id}', + ); + } + } on StateError catch (e) { + // Handle activation conflicts gracefully + if (e.message.contains('already activated') || + e.message.contains('activation in progress')) { + _log.info( + 'Activation conflict resolved for ${asset.id.id}: ${e.message}', + ); + return; + } + rethrow; + } catch (e) { + // Log activation coordination events + _logActivationCoordinationEvent(asset.id, e); + rethrow; } }, maxAttempts: maxRetryAttempts, @@ -352,7 +397,7 @@ class CoinsRepo { ), ); - _log.info('Asset activated: ${asset.id.id}'); + _log.info('Asset activated successfully: ${asset.id.id}'); if (notifyListeners) { _broadcastAsset(coin.copyWith(state: CoinState.active)); if (coin.id.parentId != null) { @@ -360,6 +405,7 @@ class CoinsRepo { _kdfSdk.assets.available[coin.id.parentId]!, ); _broadcastAsset(parentCoin.copyWith(state: CoinState.active)); + _log.fine('Parent coin also activated: ${coin.id.parentId!.id}'); } } _subscribeToBalanceUpdates(asset); @@ -373,11 +419,7 @@ class CoinsRepo { } } catch (e, s) { lastActivationException = e is Exception ? e : Exception(e.toString()); - _log.shout( - 'Error activating asset after retries: ${asset.id.id}', - e, - s, - ); + _logActivationError(asset.id, e, s); if (notifyListeners) { _broadcastAsset(asset.toCoin().copyWith(state: CoinState.suspended)); } @@ -759,12 +801,17 @@ class CoinsRepo { final inactiveAssets = await assets.removeActiveAssets(_kdfSdk); for (final asset in inactiveAssets) { final coin = coins.firstWhere((coin) => coin.id == asset.id); - await _activateZhtlcAsset( - asset, - coin, - notifyListeners: notifyListeners, - addToWalletMetadata: addToWalletMetadata, - ); + try { + await _activateZhtlcAsset( + asset, + coin, + notifyListeners: notifyListeners, + addToWalletMetadata: addToWalletMetadata, + ); + } catch (e, s) { + _log.severe('Failed to activate ZHTLC asset ${asset.id.id}', e, s); + // Continue with the next asset + } } } diff --git a/lib/bloc/transaction_history/transaction_history_bloc.dart b/lib/bloc/transaction_history/transaction_history_bloc.dart index f7c75e2ba2..56b2718bc7 100644 --- a/lib/bloc/transaction_history/transaction_history_bloc.dart +++ b/lib/bloc/transaction_history/transaction_history_bloc.dart @@ -15,25 +15,34 @@ import 'package:web_dex/shared/utils/utils.dart'; class TransactionHistoryBloc extends Bloc { - TransactionHistoryBloc({ - required KomodoDefiSdk sdk, - }) : _sdk = sdk, - super(const TransactionHistoryState.initial()) { + TransactionHistoryBloc({required KomodoDefiSdk sdk}) + : _sdk = sdk, + super(const TransactionHistoryState.initial()) { on(_onSubscribe, transformer: restartable()); on(_onStartedLoading); on(_onUpdated); on(_onFailure); + on(_onAuthStateChanged); + + // Listen to auth state changes to clear subscriptions on logout/wallet switch + _authSubscription = _sdk.auth.watchCurrentUser().listen((user) { + if (!isClosed) { + add(const TransactionHistoryAuthStateChanged()); + } + }); } final KomodoDefiSdk _sdk; StreamSubscription>? _historySubscription; StreamSubscription? _newTransactionsSubscription; + StreamSubscription? _authSubscription; // TODO: Remove or move to SDK final Set _processedTxIds = {}; @override Future close() async { + await _authSubscription?.cancel(); await _historySubscription?.cancel(); await _newTransactionsSubscription?.cancel(); return super.close(); @@ -73,47 +82,52 @@ class TransactionHistoryBloc final myAddresses = pubkeys.keys.map((p) => p.address).toSet(); // Subscribe to historical transactions - _historySubscription = - _sdk.transactions.getTransactionsStreamed(asset).listen( - (newTransactions) { - // Filter out any transactions we've already processed - final uniqueTransactions = newTransactions.where((tx) { - final isNew = !_processedTxIds.contains(tx.internalId); - if (isNew) { - _processedTxIds.add(tx.internalId); - } - return isNew; - }).toList(); - - if (uniqueTransactions.isEmpty) return; - - final sanitized = - uniqueTransactions.map((tx) => tx.sanitize(myAddresses)).toList(); - final updatedTransactions = List.of(state.transactions) - ..addAll(sanitized) - ..sort(_sortTransactions); - - if (event.coin.isErcType) { - _flagTransactions(updatedTransactions, event.coin); - } - - add(TransactionHistoryUpdated(transactions: updatedTransactions)); - }, - onError: (error) { - add( - TransactionHistoryFailure( - error: TextError(error: LocaleKeys.somethingWrong.tr()), - ), + _historySubscription = _sdk.transactions + .getTransactionsStreamed(asset) + .listen( + (newTransactions) { + // Filter out any transactions we've already processed + final uniqueTransactions = newTransactions.where((tx) { + final isNew = !_processedTxIds.contains(tx.internalId); + if (isNew) { + _processedTxIds.add(tx.internalId); + } + return isNew; + }).toList(); + + if (uniqueTransactions.isEmpty) return; + + final sanitized = uniqueTransactions + .map((tx) => tx.sanitize(myAddresses)) + .toList(); + final updatedTransactions = + List.of(state.transactions) + ..addAll(sanitized) + ..sort(_sortTransactions); + + if (event.coin.isErcType) { + _flagTransactions(updatedTransactions, event.coin); + } + + add(TransactionHistoryUpdated(transactions: updatedTransactions)); + }, + onError: (error) { + add( + TransactionHistoryFailure( + error: TextError(error: LocaleKeys.somethingWrong.tr()), + ), + ); + }, + onDone: () { + if (state.error == null && state.loading) { + add( + TransactionHistoryUpdated(transactions: state.transactions), + ); + } + // Once historical load is complete, start watching for new transactions + _subscribeToNewTransactions(asset, event.coin, myAddresses); + }, ); - }, - onDone: () { - if (state.error == null && state.loading) { - add(TransactionHistoryUpdated(transactions: state.transactions)); - } - // Once historical load is complete, start watching for new transactions - _subscribeToNewTransactions(asset, event.coin, myAddresses); - }, - ); } catch (e, s) { log( 'Error loading transaction history: $e', @@ -130,45 +144,44 @@ class TransactionHistoryBloc } void _subscribeToNewTransactions( - Asset asset, Coin coin, Set myAddresses) { - _newTransactionsSubscription = - _sdk.transactions.watchTransactions(asset).listen( - (newTransaction) { - if (_processedTxIds.contains(newTransaction.internalId)) return; - - _processedTxIds.add(newTransaction.internalId); - - final sanitized = newTransaction.sanitize(myAddresses); - final updatedTransactions = List.of(state.transactions) - ..add(sanitized) - ..sort(_sortTransactions); - - if (coin.isErcType) { - _flagTransactions(updatedTransactions, coin); - } - - add(TransactionHistoryUpdated(transactions: updatedTransactions)); - }, - onError: (error) { - add( - TransactionHistoryFailure( - error: TextError(error: LocaleKeys.somethingWrong.tr()), - ), + Asset asset, + Coin coin, + Set myAddresses, + ) { + _newTransactionsSubscription = _sdk.transactions + .watchTransactions(asset) + .listen( + (newTransaction) { + if (_processedTxIds.contains(newTransaction.internalId)) return; + + _processedTxIds.add(newTransaction.internalId); + + final sanitized = newTransaction.sanitize(myAddresses); + final updatedTransactions = List.of(state.transactions) + ..add(sanitized) + ..sort(_sortTransactions); + + if (coin.isErcType) { + _flagTransactions(updatedTransactions, coin); + } + + add(TransactionHistoryUpdated(transactions: updatedTransactions)); + }, + onError: (error) { + add( + TransactionHistoryFailure( + error: TextError(error: LocaleKeys.somethingWrong.tr()), + ), + ); + }, ); - }, - ); } void _onUpdated( TransactionHistoryUpdated event, Emitter emit, ) { - emit( - state.copyWith( - transactions: event.transactions, - loading: false, - ), - ); + emit(state.copyWith(transactions: event.transactions, loading: false)); } void _onStartedLoading( @@ -182,12 +195,24 @@ class TransactionHistoryBloc TransactionHistoryFailure event, Emitter emit, ) { - emit( - state.copyWith( - loading: false, - error: event.error, - ), - ); + emit(state.copyWith(loading: false, error: event.error)); + } + + /// Clears all transaction subscriptions when auth state changes + /// (logout or wallet switch). This prevents stale subscriptions from + /// continuing to receive updates for the previous wallet's transactions. + Future _onAuthStateChanged( + TransactionHistoryAuthStateChanged event, + Emitter emit, + ) async { + await _historySubscription?.cancel(); + _historySubscription = null; + await _newTransactionsSubscription?.cancel(); + _newTransactionsSubscription = null; + _processedTxIds.clear(); + + // Reset to initial state when auth changes + emit(const TransactionHistoryState.initial()); } } @@ -202,22 +227,20 @@ int _sortTransactions(Transaction tx1, Transaction tx2) { void _flagTransactions(List transactions, Coin coin) { if (!coin.isErcType) return; - transactions - .removeWhere((tx) => tx.balanceChanges.totalAmount.toDouble() == 0.0); + transactions.removeWhere( + (tx) => tx.balanceChanges.totalAmount.toDouble() == 0.0, + ); } class Pagination { - Pagination({ - this.fromId, - this.pageNumber, - }); + Pagination({this.fromId, this.pageNumber}); final String? fromId; final int? pageNumber; Map toJson() => { - if (fromId != null) 'FromId': fromId, - if (pageNumber != null) 'PageNumber': pageNumber, - }; + if (fromId != null) 'FromId': fromId, + if (pageNumber != null) 'PageNumber': pageNumber, + }; } /// Represents different ways to paginate transaction history @@ -230,10 +253,7 @@ sealed class TransactionPagination { /// Standard page-based pagination class PagePagination extends TransactionPagination { - const PagePagination({ - required this.pageNumber, - required this.itemsPerPage, - }); + const PagePagination({required this.pageNumber, required this.itemsPerPage}); final int pageNumber; final int itemsPerPage; diff --git a/lib/bloc/transaction_history/transaction_history_event.dart b/lib/bloc/transaction_history/transaction_history_event.dart index 55343ed847..9c40be591c 100644 --- a/lib/bloc/transaction_history/transaction_history_event.dart +++ b/lib/bloc/transaction_history/transaction_history_event.dart @@ -24,3 +24,8 @@ class TransactionHistoryFailure extends TransactionHistoryEvent { TransactionHistoryFailure({required this.error}); final BaseError error; } + +/// Event triggered when auth state changes (logout/wallet switch) +class TransactionHistoryAuthStateChanged extends TransactionHistoryEvent { + const TransactionHistoryAuthStateChanged(); +} From 8cf4d943d19609a3705b7383d7d5ebe75f858b13 Mon Sep 17 00:00:00 2001 From: Francois Date: Fri, 3 Oct 2025 18:50:32 +0200 Subject: [PATCH 2/2] revert(coin-addresses): remove auth state listener from address bloc CoinAddressesBloc is created at lower levels within buttons etc, so the auth state replay causes issues with the pubkey loading (initial state + current state = clear listeners while starting listeners) --- .../bloc/coin_addresses_bloc.dart | 61 +------------------ 1 file changed, 1 insertion(+), 60 deletions(-) diff --git a/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart b/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart index 2ef681047b..67bf64f209 100644 --- a/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart +++ b/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart' - show Asset, NewAddressStatus, AssetPubkeys, KdfUser, WalletId; + show Asset, NewAddressStatus, AssetPubkeys; import 'package:logging/logging.dart'; import 'package:web_dex/analytics/events.dart'; import 'package:web_dex/bloc/analytics/analytics_bloc.dart'; @@ -20,14 +20,6 @@ class CoinAddressesBloc extends Bloc { on(_onHideZeroBalanceChanged); on(_onPubkeysUpdated); on(_onPubkeysSubscriptionFailed); - on(_onAuthStateChanged); - - // Listen to auth state changes to clear subscriptions on logout/wallet switch - _authSubscription = _sdk.auth.watchCurrentUser().listen((user) { - if (!isClosed) { - add(CoinAddressesAuthStateChanged(user)); - } - }); } final KomodoDefiSdk _sdk; @@ -37,10 +29,6 @@ class CoinAddressesBloc extends Bloc { static final Logger _log = Logger('CoinAddressesBloc'); StreamSubscription? _pubkeysSub; - StreamSubscription? _authSubscription; - - /// Current wallet ID being tracked for auth state changes - WalletId? _currentWalletId; Future _onStarted( CoinAddressesStarted event, @@ -248,59 +236,12 @@ class CoinAddressesBloc extends Bloc { Future close() async { _log.fine('Closing CoinAddressesBloc for asset $_assetId'); - // Cancel auth subscription - try { - await _authSubscription?.cancel(); - _authSubscription = null; - } catch (e) { - _log.warning( - 'Error cancelling auth subscription for asset $_assetId: $e', - ); - } - // Cancel pubkey subscription await _cancelPubkeySubscription(); return super.close(); } - /// Clears pubkeys subscription when auth state changes (logout or wallet switch). - /// This prevents stale subscriptions from continuing to receive updates - /// for the previous wallet's addresses. - Future _onAuthStateChanged( - CoinAddressesAuthStateChanged event, - Emitter emit, - ) async { - final newWalletId = event.user?.walletId; - - _log.fine( - 'Auth state changed for asset $_assetId: ${_currentWalletId?.name} -> ' - '${newWalletId?.name}', - ); - - // If the wallet ID has changed, clear subscriptions and reset state - if (_currentWalletId != newWalletId) { - _log.info( - 'Wallet change detected for asset $_assetId, clearing pubkey subscriptions', - ); - - await _cancelPubkeySubscription(); - _currentWalletId = newWalletId; - - // Reset to initial state when wallet changes - emit(const CoinAddressesState()); - - _log.fine( - 'Auth state change handling completed for asset $_assetId, wallet: ' - '${newWalletId?.name}', - ); - } else { - _log.finest( - 'No wallet change detected for asset $_assetId, keeping current state', - ); - } - } - /// Cancels the current pubkey subscription with proper error handling Future _cancelPubkeySubscription() async { try {