From 907d43cd85eeb6408431295226923db30a69f001 Mon Sep 17 00:00:00 2001 From: Francois Date: Thu, 31 Jul 2025 11:38:40 +0200 Subject: [PATCH 1/7] fix(nft): do not add auto-activated parents to the wallet coins list --- lib/bloc/coins_bloc/coins_repo.dart | 14 +++++++++++--- lib/bloc/nfts/nft_main_repo.dart | 20 ++++++++++++-------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/lib/bloc/coins_bloc/coins_repo.dart b/lib/bloc/coins_bloc/coins_repo.dart index 28dbd6a6ff..204a83c104 100644 --- a/lib/bloc/coins_bloc/coins_repo.dart +++ b/lib/bloc/coins_bloc/coins_repo.dart @@ -253,6 +253,7 @@ class CoinsRepo { /// **Parameters:** /// - [assets]: List of assets to activate /// - [notify]: Whether to broadcast state changes to listeners (default: true) + /// - [addToWalletMetadata]: Whether to add assets to wallet metadata (default: true) /// - [maxRetryAttempts]: Maximum number of retry attempts (default: 30) /// - [initialRetryDelay]: Initial delay between retries (default: 500ms) /// - [maxRetryDelay]: Maximum delay between retries (default: 10s) @@ -269,6 +270,7 @@ class CoinsRepo { Future activateAssetsSync( List assets, { bool notify = true, + bool addToWalletMetadata = true, int maxRetryAttempts = 30, Duration initialRetryDelay = const Duration(milliseconds: 500), Duration maxRetryDelay = const Duration(seconds: 10), @@ -282,9 +284,12 @@ class CoinsRepo { return; } - // Add assets and their parents to wallet metadata before activating. - // This ensures that the wallet metadata is updated even if activation fails. - await _addAssetsToWalletMetdata(assets.map((asset) => asset.id)); + if (addToWalletMetadata) { + // Ensure the wallet metadata is updated with the assets before activation + // This is to ensure that the wallet metadata is always in sync with the assets + // being activated, even if activation fails. + await _addAssetsToWalletMetdata(assets.map((asset) => asset.id)); + } Exception? lastActivationException; @@ -391,6 +396,7 @@ class CoinsRepo { /// **Parameters:** /// - [coins]: List of coins to activate /// - [notify]: Whether to broadcast state changes to listeners (default: true) + /// - [addToWalletMetadata]: Whether to add assets to wallet metadata (default: true) /// - [maxRetryAttempts]: Maximum number of retry attempts (default: 30) /// - [initialRetryDelay]: Initial delay between retries (default: 500ms) /// - [maxRetryDelay]: Maximum delay between retries (default: 10s) @@ -410,6 +416,7 @@ class CoinsRepo { Future activateCoinsSync( List coins, { bool notify = true, + bool addToWalletMetadata = true, int maxRetryAttempts = 30, Duration initialRetryDelay = const Duration(milliseconds: 500), Duration maxRetryDelay = const Duration(seconds: 10), @@ -425,6 +432,7 @@ class CoinsRepo { return activateAssetsSync( assets, notify: notify, + addToWalletMetadata: addToWalletMetadata, maxRetryAttempts: maxRetryAttempts, initialRetryDelay: initialRetryDelay, maxRetryDelay: maxRetryDelay, diff --git a/lib/bloc/nfts/nft_main_repo.dart b/lib/bloc/nfts/nft_main_repo.dart index b3b0b014d9..50a0300d44 100644 --- a/lib/bloc/nfts/nft_main_repo.dart +++ b/lib/bloc/nfts/nft_main_repo.dart @@ -11,11 +11,9 @@ import 'package:web_dex/model/nft.dart'; import 'package:web_dex/model/text_error.dart'; class NftsRepo { - NftsRepo({ - required Mm2ApiNft api, - required CoinsRepo coinsRepo, - }) : _coinsRepo = coinsRepo, - _api = api; + NftsRepo({required Mm2ApiNft api, required CoinsRepo coinsRepo}) + : _coinsRepo = coinsRepo, + _api = api; final Logger _log = Logger('NftsRepo'); final CoinsRepo _coinsRepo; @@ -75,8 +73,9 @@ class NftsRepo { final List knownCoins = _coinsRepo.getKnownCoins(); final List parentCoins = chains .map((NftBlockchains chain) { - return knownCoins - .firstWhereOrNull((Coin coin) => coin.id.id == chain.coinAbbr()); + return knownCoins.firstWhereOrNull( + (Coin coin) => coin.id.id == chain.coinAbbr(), + ); }) .whereType() .toList(); @@ -86,7 +85,12 @@ class NftsRepo { } try { - await _coinsRepo.activateCoinsSync(parentCoins, maxRetryAttempts: 10); + await _coinsRepo.activateCoinsSync( + parentCoins, + notify: false, + addToWalletMetadata: false, + maxRetryAttempts: 10, + ); } catch (e, s) { _log.shout('Failed to activate parent coins', e, s); } From d2ab7ac4c5c0c44a5a5136045ea029e222776a73 Mon Sep 17 00:00:00 2001 From: Francois Date: Thu, 31 Jul 2025 12:12:04 +0200 Subject: [PATCH 2/7] fix(coins-repo): notify listeners for disabling in add assets page --- lib/bloc/coins_bloc/coins_repo.dart | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/bloc/coins_bloc/coins_repo.dart b/lib/bloc/coins_bloc/coins_repo.dart index 204a83c104..941f63a3db 100644 --- a/lib/bloc/coins_bloc/coins_repo.dart +++ b/lib/bloc/coins_bloc/coins_repo.dart @@ -486,20 +486,21 @@ class CoinsRepo { // Skip the deactivation step for now, as it results in "NoSuchCoin" errors // when trying to re-enable the coin later in the same session. // TODO: Revisit this and create an issue on KDF to track the problem. - // final deactivationTasks = [ - // ...coins.map((coin) async { - // await _disableCoin(coin.id.id); - // if (notify) _broadcastAsset(coin.copyWith(state: CoinState.inactive)); - // }), - // ...allChildCoins.map((child) async { - // await _disableCoin(child.id.id); - // if (notify) { - // _broadcastAsset(child.copyWith(state: CoinState.inactive)); - // } - // }), - // ]; - // await Future.wait(deactivationTasks); - + final deactivationTasks = [ + ...coins.map((coin) async { + // await _disableCoin(coin.id.id); + if (notify) { + _broadcastAsset(coin.copyWith(state: CoinState.inactive)); + } + }), + ...allChildCoins.map((child) async { + // await _disableCoin(child.id.id); + if (notify) { + _broadcastAsset(child.copyWith(state: CoinState.inactive)); + } + }), + ]; + await Future.wait(deactivationTasks); await Future.wait([...parentCancelFutures, ...childCancelFutures]); } From 15c84ebe03faf51cbbc2ddc026311ce676cecb89 Mon Sep 17 00:00:00 2001 From: Francois Date: Thu, 31 Jul 2025 14:04:34 +0200 Subject: [PATCH 3/7] fix(add-assets): sync state, remove child coins, and formatting --- .../coins_manager/coins_manager_bloc.dart | 193 +++++++++--------- 1 file changed, 94 insertions(+), 99 deletions(-) diff --git a/lib/bloc/coins_manager/coins_manager_bloc.dart b/lib/bloc/coins_manager/coins_manager_bloc.dart index b2b5e53fa7..b44503560b 100644 --- a/lib/bloc/coins_manager/coins_manager_bloc.dart +++ b/lib/bloc/coins_manager/coins_manager_bloc.dart @@ -28,20 +28,16 @@ class CoinsManagerBloc extends Bloc { required AnalyticsBloc analyticsBloc, required SettingsRepository settingsRepository, required TradingEntitiesBloc tradingEntitiesBloc, - }) : _coinsRepo = coinsRepo, - _sdk = sdk, - _analyticsBloc = analyticsBloc, - _settingsRepository = settingsRepository, - _tradingEntitiesBloc = tradingEntitiesBloc, - super(CoinsManagerState.initial(coins: [])) { + }) : _coinsRepo = coinsRepo, + _sdk = sdk, + _analyticsBloc = analyticsBloc, + _settingsRepository = settingsRepository, + _tradingEntitiesBloc = tradingEntitiesBloc, + super(CoinsManagerState.initial(coins: [])) { on(_onCoinsUpdate); on(_onCoinsListReset); on(_onCoinTypeSelect); on(_onCoinsSwitch); - // Sequential transformer is used to ensure that no concurrent updates - // occur, which could lead to inconsistent state. - // This is important for actions like selecting/deselecting coins, which - // the user might perform rapidly. on(_onCoinSelect); on(_onSelectAll); on(_onSelectedTypesReset); @@ -97,11 +93,13 @@ class CoinsManagerBloc extends Bloc { list = _sortCoins(list, event.action, state.sortData); - emit(state.copyWith( - coins: list, - action: event.action, - selectedCoins: selectedCoins, - )); + emit( + state.copyWith( + coins: list, + action: event.action, + selectedCoins: selectedCoins, + ), + ); } Future _onCoinsListReset( @@ -134,8 +132,9 @@ class CoinsManagerBloc extends Bloc { event.action, ); - final filteredCoins = - await _filterTestCoinsIfNeeded({...coins, ...selectedCoins}.toList()); + final filteredCoins = await _filterTestCoinsIfNeeded( + {...coins, ...selectedCoins}.toList(), + ); final sortedCoins = _sortCoins(filteredCoins, event.action, state.sortData); emit( @@ -194,7 +193,7 @@ class CoinsManagerBloc extends Bloc { return; } - if (selectedCoins.contains(coin)) { + if (wasSelected) { selectedCoins.remove(coin); } else { selectedCoins.add(coin); @@ -206,19 +205,18 @@ class CoinsManagerBloc extends Bloc { final bool shouldActivate = (state.action == CoinsManagerAction.add && !wasSelected) || - (state.action == CoinsManagerAction.remove && wasSelected); + (state.action == CoinsManagerAction.remove && wasSelected); if (shouldActivate) { - await _tryActivateCoin(event, coin); + await _tryActivateCoin(coin); } else { - await _tryDeactivateCoin(event, coin); + await _tryDeactivateCoin(coin); } } - Future _tryDeactivateCoin( - CoinsManagerCoinSelect event, Coin coin) async { + Future _tryDeactivateCoin(Coin coin) async { try { - await _coinsRepo.deactivateCoinsSync([event.coin]); + await _coinsRepo.deactivateCoinsSync([coin]); } catch (e, s) { _log.warning('Failed to deactivate coin ${coin.abbr}', e, s); } @@ -232,9 +230,9 @@ class CoinsManagerBloc extends Bloc { ); } - Future _tryActivateCoin(CoinsManagerCoinSelect event, Coin coin) async { + Future _tryActivateCoin(Coin coin) async { try { - await _coinsRepo.activateCoinsSync([event.coin]); + await _coinsRepo.activateCoinsSync([coin]); } catch (e, s) { _log.warning('Failed to activate coin ${coin.abbr}', e, s); } @@ -252,8 +250,9 @@ class CoinsManagerBloc extends Bloc { CoinsManagerSelectAllTap event, Emitter emit, ) { - final selectedCoins = - state.isSelectedAllCoinsEnabled ? [] : state.coins; + final selectedCoins = state.isSelectedAllCoinsEnabled + ? [] + : state.coins; emit(state.copyWith(selectedCoins: selectedCoins)); } @@ -277,8 +276,11 @@ class CoinsManagerBloc extends Bloc { CoinsManagerSortChanged event, Emitter emit, ) { - final List sorted = - _sortCoins([...state.coins], state.action, event.sortData); + final List sorted = _sortCoins( + [...state.coins], + state.action, + event.sortData, + ); emit(state.copyWith(coins: sorted, sortData: event.sortData)); } @@ -289,10 +291,15 @@ class CoinsManagerBloc extends Bloc { List _filterByPhrase(List coins) { final String filter = state.searchPhrase.toLowerCase(); - final List filtered = filter.isEmpty - ? coins - : coins.where((Coin coin) => compareCoinByPhrase(coin, filter)).toList() - ..sort((a, b) => a.abbr.toLowerCase().compareTo(b.abbr.toLowerCase())); + final List filtered = + filter.isEmpty + ? coins + : coins + .where((Coin coin) => compareCoinByPhrase(coin, filter)) + .toList() + ..sort( + (a, b) => a.abbr.toLowerCase().compareTo(b.abbr.toLowerCase()), + ); return filtered; } @@ -367,51 +374,62 @@ class CoinsManagerBloc extends Bloc { // Find child coins (tokens) final walletCoins = await _coinsRepo.getWalletCoins(); - final childCoins = - walletCoins.where((c) => c.parentCoin?.abbr == coin.abbr).toList(); + final childCoins = walletCoins + .where((c) => c.parentCoin?.abbr == coin.abbr) + .toList(); // Check for active swaps - final hasSwap = _tradingEntitiesBloc.hasActiveSwap(coin.abbr) || + final hasSwap = + _tradingEntitiesBloc.hasActiveSwap(coin.abbr) || childCoins.any((c) => _tradingEntitiesBloc.hasActiveSwap(c.abbr)); if (hasSwap) { - emit(state.copyWith( - removalState: CoinRemovalState( - coin: coin, - childCoins: childCoins, - blockReason: CoinRemovalBlockReason.activeSwap, - openOrdersCount: 0, + emit( + state.copyWith( + removalState: CoinRemovalState( + coin: coin, + childCoins: childCoins, + blockReason: CoinRemovalBlockReason.activeSwap, + openOrdersCount: 0, + ), ), - )); + ); return; } // Check for open orders - final int openOrders = _tradingEntitiesBloc.openOrdersCount(coin.abbr) + + final int openOrders = + _tradingEntitiesBloc.openOrdersCount(coin.abbr) + childCoins.fold( - 0, (sum, c) => sum + _tradingEntitiesBloc.openOrdersCount(c.abbr)); + 0, + (sum, c) => sum + _tradingEntitiesBloc.openOrdersCount(c.abbr), + ); if (openOrders > 0) { - emit(state.copyWith( - removalState: CoinRemovalState( - coin: coin, - childCoins: childCoins, - blockReason: CoinRemovalBlockReason.openOrders, - openOrdersCount: openOrders, + emit( + state.copyWith( + removalState: CoinRemovalState( + coin: coin, + childCoins: childCoins, + blockReason: CoinRemovalBlockReason.openOrders, + openOrdersCount: openOrders, + ), ), - )); + ); return; } // No blocking conditions, proceed with confirmation flow - emit(state.copyWith( - removalState: CoinRemovalState( - coin: coin, - childCoins: childCoins, - blockReason: CoinRemovalBlockReason.none, - openOrdersCount: 0, + emit( + state.copyWith( + removalState: CoinRemovalState( + coin: coin, + childCoins: childCoins, + blockReason: CoinRemovalBlockReason.none, + openOrdersCount: 0, + ), ), - )); + ); } Future _onCoinRemoveConfirmed( @@ -434,51 +452,28 @@ class CoinsManagerBloc extends Bloc { } catch (e, s) { _log.warning('Failed to cancel orders for coin ${coin.abbr}', e, s); - // Clear removal state and emit error message - emit(state.copyWith( - removalState: null, - errorMessage: - 'Failed to cancel open orders for ${coin.abbr}. Please try again.', - )); + emit( + state.copyWith( + removalState: null, + errorMessage: + 'Failed to cancel open orders for ${coin.abbr}. Please try again.', + ), + ); return; } } - // Remove coin from selected coins if in add mode (deselection) - // or proceed with actual removal if in remove mode - if (state.action == CoinsManagerAction.add) { - // Deselect the coin and all its child coins - final selectedCoins = Set.from(state.selectedCoins); - selectedCoins.remove(coin); - - // Also remove all child coins from selected coins - for (final childCoin in childCoins) { - selectedCoins.remove(childCoin); - } - - emit(state.copyWith( - selectedCoins: selectedCoins.toList(), - removalState: null, - )); + final selectedCoins = Set.from(state.selectedCoins) + ..remove(coin) + ..removeAll(childCoins); - // Deactivate the coin - try { - await _tryDeactivateCoin(CoinsManagerCoinSelect(coin: coin), coin); - } catch (e, s) { - _log.warning( - 'Failed to deactivate coin ${coin.abbr} after removal confirmation', - e, - s); - // Note: The coin is already removed from selectedCoins, so the UI state is consistent - // even if deactivation fails - } - } else { - // Clear removal state and proceed with removal via existing logic - emit(state.copyWith(removalState: null)); + // Emit state immediately for responsive UI + // before performing the actual activation/deactivation in background + emit( + state.copyWith(removalState: null, selectedCoins: selectedCoins.toList()), + ); - // Proceed with actual coin removal for remove mode - add(CoinsManagerCoinSelect(coin: coin)); - } + await _tryDeactivateCoin(coin); } void _onCoinRemovalCancelled( From e55f36627c81e1129a25b28fe55575ec72bbaa3e Mon Sep 17 00:00:00 2001 From: Francois Date: Thu, 31 Jul 2025 15:09:06 +0200 Subject: [PATCH 4/7] fix(add-assets): deduplicate coins list legacy usdPrice and address fields were added to the Coin model props, which could result in duplication when those fields are updated while the user is filtering --- .../coins_manager/coins_manager_bloc.dart | 44 +++++---- lib/model/coin.dart | 94 +++++++++++-------- 2 files changed, 75 insertions(+), 63 deletions(-) diff --git a/lib/bloc/coins_manager/coins_manager_bloc.dart b/lib/bloc/coins_manager/coins_manager_bloc.dart index b44503560b..f80e5c91a4 100644 --- a/lib/bloc/coins_manager/coins_manager_bloc.dart +++ b/lib/bloc/coins_manager/coins_manager_bloc.dart @@ -62,10 +62,10 @@ class CoinsManagerBloc extends Bloc { ) async { final List filters = []; - List list = mergeCoinLists( + final mergedCoinsList = mergeCoinLists( await _getOriginalCoinList(_coinsRepo, event.action), state.coins, - ); + ).toList(); // Add wallet coins to selected coins if in add mode so that they // are displayed in the list with the checkbox selected. This is @@ -76,9 +76,11 @@ class CoinsManagerBloc extends Bloc { event.action, ); - final uniqueCombinedList = {...list, ...selectedCoins}.toList(); + final uniqueCombinedList = {...mergedCoinsList, ...selectedCoins}; - list = await _filterTestCoinsIfNeeded(uniqueCombinedList); + final testFilteredCoins = await _filterTestCoinsIfNeeded( + uniqueCombinedList.toList(), + ); if (state.searchPhrase.isNotEmpty) { filters.add(_filterByPhrase); @@ -87,15 +89,16 @@ class CoinsManagerBloc extends Bloc { filters.add(_filterByType); } + List filteredCoins = testFilteredCoins; for (final filter in filters) { - list = filter(list); + filteredCoins = filter(filteredCoins); } - list = _sortCoins(list, event.action, state.sortData); + final sortedCoins = _sortCoins(filteredCoins, event.action, state.sortData); emit( state.copyWith( - coins: list, + coins: sortedCoins.unique((coin) => coin.id), action: event.action, selectedCoins: selectedCoins, ), @@ -139,7 +142,7 @@ class CoinsManagerBloc extends Bloc { emit( state.copyWith( - coins: sortedCoins, + coins: sortedCoins.unique((coin) => coin.id), action: event.action, selectedCoins: selectedCoins, ), @@ -291,16 +294,12 @@ class CoinsManagerBloc extends Bloc { List _filterByPhrase(List coins) { final String filter = state.searchPhrase.toLowerCase(); - final List filtered = - filter.isEmpty - ? coins - : coins - .where((Coin coin) => compareCoinByPhrase(coin, filter)) - .toList() - ..sort( - (a, b) => a.abbr.toLowerCase().compareTo(b.abbr.toLowerCase()), - ); - return filtered; + return filter.isEmpty + ? coins.toList() + : coins + .where((Coin coin) => compareCoinByPhrase(coin, filter)) + .toList() + ..sort((a, b) => a.abbr.toLowerCase().compareTo(b.abbr.toLowerCase())); } List _filterByType(List coins) { @@ -331,7 +330,7 @@ class CoinsManagerBloc extends Bloc { return result; } - List mergeCoinLists(List originalList, List newList) { + Set mergeCoinLists(List originalList, List newList) { final Map coinMap = {}; for (final Coin coin in originalList) { @@ -342,8 +341,7 @@ class CoinsManagerBloc extends Bloc { coinMap[coin.id.id] = coin; } - final list = coinMap.values.toList(); - return list; + return coinMap.values.toSet(); } List _sortCoins( @@ -463,9 +461,9 @@ class CoinsManagerBloc extends Bloc { } } - final selectedCoins = Set.from(state.selectedCoins) + final selectedCoins = state.selectedCoins ..remove(coin) - ..removeAll(childCoins); + ..removeWhere((coin) => childCoins.any((child) => child.id == coin.id)); // Emit state immediately for responsive UI // before performing the actual activation/deactivation in background diff --git a/lib/model/coin.dart b/lib/model/coin.dart index b1bc425828..3b84b99484 100644 --- a/lib/model/coin.dart +++ b/lib/model/coin.dart @@ -174,32 +174,32 @@ class Coin extends Equatable { bool? isCustomCoin, }) { return Coin( - type: type ?? this.type, - abbr: abbr ?? this.abbr, - id: id ?? this.id, - name: name ?? this.name, - logoImageUrl: logoImageUrl ?? this.logoImageUrl, - explorerUrl: explorerUrl ?? this.explorerUrl, - explorerTxUrl: explorerTxUrl ?? this.explorerTxUrl, - explorerAddressUrl: explorerAddressUrl ?? this.explorerAddressUrl, - protocolType: protocolType ?? this.protocolType, - protocolData: protocolData ?? this.protocolData, - isTestCoin: isTestCoin ?? this.isTestCoin, - coingeckoId: coingeckoId ?? this.coingeckoId, - fallbackSwapContract: fallbackSwapContract ?? this.fallbackSwapContract, - priority: priority ?? this.priority, - state: state ?? this.state, - decimals: decimals ?? this.decimals, - parentCoin: parentCoin ?? this.parentCoin, - derivationPath: derivationPath ?? this.derivationPath, - usdPrice: usdPrice ?? this.usdPrice, - coinpaprikaId: coinpaprikaId ?? this.coinpaprikaId, - activeByDefault: activeByDefault ?? this.activeByDefault, - swapContractAddress: swapContractAddress ?? _swapContractAddress, - walletOnly: walletOnly ?? _walletOnly, - mode: mode ?? this.mode, - isCustomCoin: isCustomCoin ?? this.isCustomCoin, - ) + type: type ?? this.type, + abbr: abbr ?? this.abbr, + id: id ?? this.id, + name: name ?? this.name, + logoImageUrl: logoImageUrl ?? this.logoImageUrl, + explorerUrl: explorerUrl ?? this.explorerUrl, + explorerTxUrl: explorerTxUrl ?? this.explorerTxUrl, + explorerAddressUrl: explorerAddressUrl ?? this.explorerAddressUrl, + protocolType: protocolType ?? this.protocolType, + protocolData: protocolData ?? this.protocolData, + isTestCoin: isTestCoin ?? this.isTestCoin, + coingeckoId: coingeckoId ?? this.coingeckoId, + fallbackSwapContract: fallbackSwapContract ?? this.fallbackSwapContract, + priority: priority ?? this.priority, + state: state ?? this.state, + decimals: decimals ?? this.decimals, + parentCoin: parentCoin ?? this.parentCoin, + derivationPath: derivationPath ?? this.derivationPath, + usdPrice: usdPrice ?? this.usdPrice, + coinpaprikaId: coinpaprikaId ?? this.coinpaprikaId, + activeByDefault: activeByDefault ?? this.activeByDefault, + swapContractAddress: swapContractAddress ?? _swapContractAddress, + walletOnly: walletOnly ?? _walletOnly, + mode: mode ?? this.mode, + isCustomCoin: isCustomCoin ?? this.isCustomCoin, + ) ..address = address ?? this.address ..sendableBalance = sendableBalance ?? this.sendableBalance; } @@ -208,11 +208,11 @@ class Coin extends Equatable { // legacy fields here. @override List get props => [ - id, - // Legacy fields still updated and used in the app, so we keep them - // in the props list for now to maintain the desired state updates. - state, type, abbr, usdPrice, isTestCoin, parentCoin, address, - ]; + id, + // Legacy fields still updated and used in the app, so we keep them + // in the props list for now to maintain the desired state updates. + state, type, abbr, usdPrice, isTestCoin, parentCoin, address, + ]; } extension LegacyCoinToSdkAsset on Coin { @@ -227,8 +227,8 @@ class ProtocolData { factory ProtocolData.fromJson(Map json) => ProtocolData( platform: json['platform'], - contractAddress: json['contract_address'] ?? '', - ); + contractAddress: json['contract_address'] ?? '', + ); String platform; String contractAddress; @@ -244,17 +244,17 @@ class ProtocolData { class CoinNode { const CoinNode({required this.url, required this.guiAuth}); static CoinNode fromJson(Map json) => CoinNode( - url: json['url'], - guiAuth: (json['gui_auth'] ?? json['komodo_proxy']) ?? false, - ); + url: json['url'], + guiAuth: (json['gui_auth'] ?? json['komodo_proxy']) ?? false, + ); final bool guiAuth; final String url; Map toJson() => { - 'url': url, - 'gui_auth': guiAuth, - 'komodo_proxy': guiAuth, - }; + 'url': url, + 'gui_auth': guiAuth, + 'komodo_proxy': guiAuth, + }; } enum CoinMode { segwit, standard, hw } @@ -275,3 +275,17 @@ extension CoinListExtension on List { const String _urgentDeprecationNotice = '(URGENT) This must be fixed before the next release.'; + +/// Extension to filter a list of coins to unique elements based on a given ID function. +/// If no ID function is provided, the elements themselves are used as IDs. +/// +/// Helper method to get unique items from a list, given that the equality check for Coin is +/// based on transient fields that can change for the same coin. +extension Unique on List { + List unique(Id Function(E element) id, [bool inplace = true]) { + final ids = {}; + var list = inplace ? this : List.from(this); + list.retainWhere((x) => ids.add(id(x))); + return list; + } +} From 111dbe201175cf0e7b7a02a2562c87b6e18717b8 Mon Sep 17 00:00:00 2001 From: Francois Date: Thu, 31 Jul 2025 15:55:25 +0200 Subject: [PATCH 5/7] fix(coins-manager): create copy of selectedCoins Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/bloc/coins_manager/coins_manager_bloc.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bloc/coins_manager/coins_manager_bloc.dart b/lib/bloc/coins_manager/coins_manager_bloc.dart index f80e5c91a4..d253b8f667 100644 --- a/lib/bloc/coins_manager/coins_manager_bloc.dart +++ b/lib/bloc/coins_manager/coins_manager_bloc.dart @@ -461,7 +461,7 @@ class CoinsManagerBloc extends Bloc { } } - final selectedCoins = state.selectedCoins + final selectedCoins = List.from(state.selectedCoins) ..remove(coin) ..removeWhere((coin) => childCoins.any((child) => child.id == coin.id)); From ca7f7f26c8e4d435ec1dc20e441c56f20324228b Mon Sep 17 00:00:00 2001 From: Francois Date: Thu, 31 Jul 2025 17:23:13 +0200 Subject: [PATCH 6/7] fix(private-keys): filter out excluded assets from the private keys --- lib/app_config/app_config.dart | 26 ++++++++++--------- .../security_settings_page.dart | 4 ++- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/app_config/app_config.dart b/lib/app_config/app_config.dart index 703e0df94c..551b99947e 100644 --- a/lib/app_config/app_config.dart +++ b/lib/app_config/app_config.dart @@ -17,7 +17,8 @@ const String assetsPath = 'assets'; const String coinsAssetsPath = 'packages/komodo_defi_framework/assets'; final Uri discordSupportChannelUrl = Uri.parse( - 'https://discord.com/channels/412898016371015680/429676282196787200'); + 'https://discord.com/channels/412898016371015680/429676282196787200', +); final Uri discordInviteUrl = Uri.parse('https://komodoplatform.com/discord'); /// Const to define if Bitrefill integration is enabled in the app. @@ -92,6 +93,7 @@ Map priorityCoinsAbbrMap = { /// List of coins that are excluded from the list of coins displayed on the /// coin lists (e.g. wallet page, coin selection dropdowns, etc.) +/// TODO: remove this list once zhltc and NFTs are fully supported in the SDK const Set excludedAssetList = { 'ADEXBSCT', 'ADEXBSC', @@ -161,17 +163,17 @@ const List appWalletOnlyAssetList = [ /// Coins that are enabled by default on restore from seed or registration. /// This will not affect existing wallets. List get enabledByDefaultCoins => [ - 'KMD', // Always included (Komodo ecosystem) - 'BTC-segwit', // Bitcoin (Rank 1, ~$2.21T market cap) - 'ETH', // Ethereum (Rank 2, ~$335B market cap) - 'BNB', // Binance Coin (Rank 5, ~$93B market cap) - 'DOGE', // Dogecoin (Rank 9, ~$27.1B market cap) - 'LTC-segwit', // Litecoin (popular, has segwit support) - 'USDT-ERC20', // Tether on Ethereum (most common stablecoin) - 'XRP-ERC20', // XRP (Rank 4, ~$145B market cap) - if (kDebugMode) 'DOC', - if (kDebugMode) 'MARTY', - ]; + 'KMD', // Always included (Komodo ecosystem) + 'BTC-segwit', // Bitcoin (Rank 1, ~$2.21T market cap) + 'ETH', // Ethereum (Rank 2, ~$335B market cap) + 'BNB', // Binance Coin (Rank 5, ~$93B market cap) + 'DOGE', // Dogecoin (Rank 9, ~$27.1B market cap) + 'LTC-segwit', // Litecoin (popular, has segwit support) + 'USDT-ERC20', // Tether on Ethereum (most common stablecoin) + 'XRP-ERC20', // XRP (Rank 4, ~$145B market cap) + if (kDebugMode) 'DOC', + if (kDebugMode) 'MARTY', +]; List get coinsWithFaucet => ['RICK', 'MORTY', 'DOC', 'MARTY']; diff --git a/lib/views/settings/widgets/security_settings/security_settings_page.dart b/lib/views/settings/widgets/security_settings/security_settings_page.dart index 265ec0be07..02daf2c6ea 100644 --- a/lib/views/settings/widgets/security_settings/security_settings_page.dart +++ b/lib/views/settings/widgets/security_settings/security_settings_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; 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'; +import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/security_settings/security_settings_bloc.dart'; import 'package:web_dex/bloc/security_settings/security_settings_event.dart'; @@ -241,7 +242,8 @@ class _SecuritySettingsPageState extends State { try { // Fetch private keys directly into local UI state // This keeps sensitive data in minimal scope - _sdkPrivateKeys = await context.sdk.security.getPrivateKeys(); + _sdkPrivateKeys = (await context.sdk.security.getPrivateKeys()) + ..removeWhere((asset, _) => excludedAssetList.contains(asset.id)); return true; // Success } catch (e) { From 566185987494b42bdbca431e3d2cb5c835fba4f6 Mon Sep 17 00:00:00 2001 From: Francois Date: Thu, 31 Jul 2025 21:21:56 +0200 Subject: [PATCH 7/7] refactor(private-keys): clone list from SDK before mutating --- .../widgets/security_settings/security_settings_page.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/views/settings/widgets/security_settings/security_settings_page.dart b/lib/views/settings/widgets/security_settings/security_settings_page.dart index 02daf2c6ea..0924b13b7d 100644 --- a/lib/views/settings/widgets/security_settings/security_settings_page.dart +++ b/lib/views/settings/widgets/security_settings/security_settings_page.dart @@ -242,8 +242,11 @@ class _SecuritySettingsPageState extends State { try { // Fetch private keys directly into local UI state // This keeps sensitive data in minimal scope - _sdkPrivateKeys = (await context.sdk.security.getPrivateKeys()) - ..removeWhere((asset, _) => excludedAssetList.contains(asset.id)); + final privateKeys = await context.sdk.security.getPrivateKeys(); + final filteredPrivateKeyEntries = privateKeys.entries.where( + (entry) => !excludedAssetList.contains(entry.key.id), + ); + _sdkPrivateKeys = Map.fromEntries(filteredPrivateKeyEntries); return true; // Success } catch (e) {