Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 25 additions & 15 deletions lib/bloc/coins_bloc/coins_repo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ class CoinsRepo {
Exception? lastActivationException;

for (final asset in assets) {
final coin = asset.toCoin();
final coin = _assetToCoinWithoutAddress(asset);
try {
if (notify) _broadcastAsset(coin.copyWith(state: CoinState.activating));

Expand Down Expand Up @@ -416,7 +416,10 @@ class CoinsRepo {
}) async {
final assets = coins
.map((coin) => _kdfSdk.assets.available[coin.id])
.whereType<Asset>()
// use cast instead of `whereType` to ensure an exception is thrown
// if the provided asset is not found in the SDK. An explicit
// argument error might be more apt here.
.cast<Asset>()
.toList();

return activateAssetsSync(
Expand All @@ -432,6 +435,10 @@ class CoinsRepo {
/// If [notify] is true, it will broadcast the deactivation to listeners.
/// This method is used to deactivate coins that are no longer needed or
/// supported by the user.
///
/// NOTE: Only balance watchers are cancelled, the coins are not deactivated
/// in the SDK or MM2. This is a temporary solution to avoid "NoSuchCoin"
/// errors when trying to re-enable the coin later in the same session.
Future<void> deactivateCoinsSync(
List<Coin> coins, {
bool notify = true,
Expand Down Expand Up @@ -468,20 +475,23 @@ class CoinsRepo {
_balanceWatchers.remove(child.id);
});

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));
}
}),
];
// 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);
Comment on lines +478 to +493
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Address the unused _disableCoin method and track the TODO.

The commented-out deactivation logic creates technical debt. The _disableCoin method is now unused (flagged by pipeline), and the TODO should be properly tracked.

Consider these actions:

  1. Remove the unused _disableCoin method since it's no longer called
  2. Create a proper issue to track the TODO comment about revisiting coin deactivation
  3. Add a reference to the created issue in the TODO comment

🏁 Script executed:

#!/bin/bash
# Verify that _disableCoin method is indeed unused throughout the codebase
rg -A 5 "_disableCoin" --type dart

Length of output: 1188


Remove unused deactivation logic and properly track the TODO

The private method _disableCoin is only referenced by the commented‐out block and is never invoked at runtime, and leaving both the dead code and the TODO in place creates unnecessary technical debt. Please:

  • In lib/bloc/coins_bloc/coins_repo.dart:
    • Remove the entire commented-out deactivation tasks block (around lines 475–490).
    • Delete the unused _disableCoin(String coinId) method definition (just below the commented block).
  • Create a new issue (e.g. KDF-XXXX) to investigate and resolve the “NoSuchCoin” errors when re-enabling coins in the same session.
  • Update the existing TODO comment to reference the newly created issue, for example:
    // TODO(KDF-XXXX): revisit coin deactivation and re-enable path
🤖 Prompt for AI Agents
In lib/bloc/coins_bloc/coins_repo.dart around lines 475 to 490, remove the
entire commented-out block containing the deactivation tasks since it is unused
and adds technical debt. Also delete the private method _disableCoin(String
coinId) defined just below this block as it is no longer called. Create a new
issue (e.g., KDF-XXXX) to track the “NoSuchCoin” errors when re-enabling coins,
and update the existing TODO comment to reference this issue with the format: //
TODO(KDF-XXXX): revisit coin deactivation and re-enable path.


await Future.wait(deactivationTasks);
await Future.wait([...parentCancelFutures, ...childCancelFutures]);
}

Expand Down
21 changes: 19 additions & 2 deletions lib/bloc/nfts/nft_main_bloc.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:equatable/equatable.dart';
import 'package:komodo_defi_sdk/komodo_defi_sdk.dart';
import 'package:komodo_defi_types/komodo_defi_type_utils.dart'
show retry, ExponentialBackoff;
import 'package:komodo_defi_types/komodo_defi_types.dart';
import 'package:logging/logging.dart';
import 'package:web_dex/bloc/nfts/nft_main_repo.dart';
Expand All @@ -20,7 +23,8 @@ class NftMainBloc extends Bloc<NftMainEvent, NftMainState> {
}) : _repo = repo,
_sdk = sdk,
super(NftMainState.initial()) {
on<NftMainChainUpdateRequested>(_onChainNftsUpdateRequested);
on<NftMainChainUpdateRequested>(_onChainNftsUpdateRequested,
transformer: restartable());
on<NftMainTabChanged>(_onTabChanged);
on<NftMainResetRequested>(_onReset);
on<NftMainChainNftsRefreshed>(_onRefreshForChain);
Expand Down Expand Up @@ -49,6 +53,8 @@ class NftMainBloc extends Bloc<NftMainEvent, NftMainState> {
) async {
emit(state.copyWith(selectedChain: () => event.chain));
if (!await _sdk.auth.isSignedIn() || !state.isInitialized) {
_log.warning(
'User is not signed in or state is not initialized. Cannot change NFT tab.');
return;
}

Expand Down Expand Up @@ -80,11 +86,13 @@ class NftMainBloc extends Bloc<NftMainEvent, NftMainState> {
Emitter<NftMainState> emit,
) async {
if (!await _sdk.auth.isSignedIn()) {
_log.warning('User is not signed in. Cannot update NFT chains.');
return;
}

try {
_log.info('Updating all NFT chains');

final Map<NftBlockchains, List<NftToken>> nfts = await _getAllNfts();
final (counts, sortedChains) = _calculateNftCount(nfts);

Expand Down Expand Up @@ -174,7 +182,16 @@ class NftMainBloc extends Bloc<NftMainEvent, NftMainState> {
Future<Map<NftBlockchains, List<NftToken>>> _getAllNfts({
List<NftBlockchains> chains = NftBlockchains.values,
}) async {
await _repo.updateNft(chains);
try {
await retry<void>(
() async => await _repo.updateNft(chains),
maxAttempts: 3,
backoffStrategy:
ExponentialBackoff(initialDelay: const Duration(seconds: 1)),
);
} catch (e, s) {
_log.severe('Error updating NFTs for chains $chains', e, s);
}
final List<NftToken> list = await _repo.getNfts(chains);

final Map<NftBlockchains, List<NftToken>> nfts =
Expand Down
50 changes: 37 additions & 13 deletions lib/bloc/nfts/nft_main_repo.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:komodo_defi_types/komodo_defi_type_utils.dart';
import 'package:logging/logging.dart';
import 'package:web_dex/bloc/coins_bloc/coins_repo.dart';
import 'package:web_dex/generated/codegen_loader.g.dart';
import 'package:web_dex/mm2/mm2_api/mm2_api_nft.dart';
import 'package:web_dex/mm2/mm2_api/rpc/errors.dart';
import 'package:web_dex/mm2/mm2_api/rpc/nft/get_nft_list/get_nft_list_res.dart';
import 'package:web_dex/model/coin.dart' show Coin;
import 'package:web_dex/model/nft.dart';
import 'package:web_dex/model/text_error.dart';
import 'package:web_dex/shared/utils/utils.dart';

class NftsRepo {
NftsRepo({
Expand All @@ -15,32 +17,29 @@ class NftsRepo {
}) : _coinsRepo = coinsRepo,
_api = api;

final Logger _log = Logger('NftsRepo');
final CoinsRepo _coinsRepo;
final Mm2ApiNft _api;

Future<void> updateNft(List<NftBlockchains> chains) async {
// Only runs on active nft chains
// Ensure that the parent coins for the NFT chains are activated.
await _activateParentCoins(chains);
await _api.enableNftChains(chains);
final json = await _api.updateNftList(chains);
if (json['error'] != null) {
log(
json['error'] as String,
path: 'nft_main_repo => updateNft',
isError: true,
).ignore();
_log.severe(json['error'] as String);
throw ApiError(message: json['error'] as String);
}
}

Future<List<NftToken>> getNfts(List<NftBlockchains> chains) async {
// Only runs on active nft chains
// Ensure that the parent coins for the NFT chains are activated.
await _activateParentCoins(chains);
await _api.enableNftChains(chains);
final json = await _api.getNftList(chains);
final jsonError = json['error'] as String?;
if (jsonError != null) {
log(
jsonError,
path: 'nft_main_repo => getNfts',
isError: true,
).ignore();
_log.severe(jsonError);
if (jsonError.toLowerCase().startsWith('transport')) {
throw TransportError(message: jsonError);
} else {
Expand All @@ -67,4 +66,29 @@ class NftsRepo {
throw ParsingApiJsonError(message: 'nft_main_repo -> getNfts: $e');
}
}

/// Ensures that the parent coins for the provided NFT chains are activated.
///
/// TODO: Migrate NFT functionality to the SDK. This is a temporary measure
/// during the transition period.
Future<void> _activateParentCoins(List<NftBlockchains> chains) async {
final List<Coin> knownCoins = _coinsRepo.getKnownCoins();
final List<Coin> parentCoins = chains
.map((NftBlockchains chain) {
return knownCoins
.firstWhereOrNull((Coin coin) => coin.id.id == chain.coinAbbr());
})
.whereType<Coin>()
.toList();

if (parentCoins.isEmpty) {
return;
}

try {
await _coinsRepo.activateCoinsSync(parentCoins, maxRetryAttempts: 10);
} catch (e, s) {
_log.shout('Failed to activate parent coins', e, s);
}
}
}
41 changes: 31 additions & 10 deletions lib/mm2/mm2_api/mm2_api_nft.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
// TODO: update [TransportError] and [BaseError] to either use SDK exceptions
// or to at least extend the Exception class
// ignore_for_file: only_throw_errors

import 'dart:convert';

import 'package:http/http.dart';
Expand Down Expand Up @@ -38,7 +34,6 @@ class Mm2ApiNft {
'while your NFTs are loaded.',
};
}
await _enableNftChains(chains);
final request = UpdateNftRequest(chains: nftChains);

return await call(request);
Expand Down Expand Up @@ -125,33 +120,45 @@ class Mm2ApiNft {
}
}

/// Returns a list of the [chains] that are currently active in KDF via the SDK.
/// This is used to ensure that the NFT functionality only operates on
/// chains that are activated in the SDK.
/// If no chains are active, an empty list is returned.
Future<List<String>> getActiveNftChains(List<NftBlockchains> chains) async {
final List<Asset> apiCoins = await _sdk.assets.getActivatedAssets();
final List<String> enabledCoinIds = apiCoins.map((c) => c.id.id).toList();
_log.fine('enabledCoinIds: $enabledCoinIds');
final List<String> nftCoins = chains.map((c) => c.coinAbbr()).toList();
_log.fine('nftCoins: $nftCoins');

final List<NftBlockchains> activeChains = chains
.map((c) => c)
.toList()
.where((c) => enabledCoinIds.contains(c.coinAbbr()))
.toList();
_log.fine('activeChains: $activeChains');

final List<String> nftChains =
activeChains.map((c) => c.toApiRequest()).toList();
_log.fine('nftChains: $nftChains');

return nftChains;
}

Future<void> enableNft(Asset asset) async {
final configSymbol = asset.id.symbol.configSymbol;
final configSymbol = asset.id.symbol.assetConfigId;
final activationParams =
NftActivationParams(provider: NftProvider.moralis());
await _sdk.client.rpc.nft
.enableNft(ticker: configSymbol, activationParams: activationParams);
await retry<void>(
() async => await _sdk.client.rpc.nft
.enableNft(ticker: configSymbol, activationParams: activationParams),
maxAttempts: 3,
backoffStrategy:
ExponentialBackoff(initialDelay: const Duration(seconds: 1)),
);
}

Future<void> _enableNftChains(
Future<void> enableNftChains(
List<NftBlockchains> chains,
) async {
final knownAssets = _sdk.assets.available;
Expand All @@ -166,12 +173,26 @@ class Mm2ApiNft {
.firstWhere((asset) => asset.id.id == chain.nftAssetTicker()),
)
.toList();

if (inactiveChains.isEmpty) {
return;
}

// Attempt to enable all inactive NFT chains, logging any errors.
// but not throwing them immediately, so we can try to enable all chains.
// If any chain fails, we will throw the last error encountered.
Exception? lastError;
for (final chain in inactiveChains) {
await enableNft(chain);
try {
await enableNft(chain);
} catch (e) {
_log.shout('Failed to enable NFT chain: ${chain.id.id}', e);
lastError = e as Exception;
}
}

if (lastError != null) {
throw lastError;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import 'package:flutter/material.dart';

class SkeletonListTile extends StatefulWidget {
const SkeletonListTile({super.key});
const SkeletonListTile({
super.key,
this.height = 122,
});

final double height;

@override
State<SkeletonListTile> createState() => _SkeletonListTileState();
Expand Down Expand Up @@ -49,6 +54,7 @@ class _SkeletonListTileState extends State<SkeletonListTile>
@override
Widget build(BuildContext context) {
return Container(
height: widget.height,
padding: const EdgeInsets.all(16),
child: Row(
children: <Widget>[
Expand Down
Loading