Skip to content
2 changes: 1 addition & 1 deletion packages/komodo_coins/lib/src/komodo_coins_base.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:komodo_coins/src/config_transform.dart';
import 'package:komodo_coins/src/asset_filter.dart';
import 'package:komodo_coins/src/config_transform.dart';
import 'package:komodo_defi_types/komodo_defi_type_utils.dart';
import 'package:komodo_defi_types/komodo_defi_types.dart';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ class _AssetItemTrailing extends StatelessWidget {

@override
Widget build(BuildContext context) {
final isChildAsset = asset.id.isChildAsset;

// Use the parent coin ticker for child assets so that token logos display
// the network they belong to (e.g. ETH for ERC20 tokens).
final protocolTicker =
isChildAsset ? asset.id.parentId?.id : asset.id.subClass.iconTicker;

return Row(
mainAxisSize: MainAxisSize.min,
children: [
Expand All @@ -76,7 +83,7 @@ class _AssetItemTrailing extends StatelessWidget {
CircleAvatar(
radius: 12,
foregroundImage: NetworkImage(
'https://komodoplatform.github.io/coins/icons/${asset.id.subClass.iconTicker.toLowerCase()}.png',
'https://komodoplatform.github.io/coins/icons/${protocolTicker?.toLowerCase()}.png',
),
backgroundColor: Colors.white70,
),
Expand Down
5 changes: 3 additions & 2 deletions packages/komodo_defi_types/lib/src/assets/asset_id.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ class AssetId extends Equatable {
? null
: knownIds?.singleWhere(
(parent) =>
parent.id == parentCoinTicker && parent.subClass == subClass,
parent.id == parentCoinTicker &&
parent.subClass.canBeParentOf(subClass),
);

return AssetId(
Expand Down Expand Up @@ -127,7 +128,7 @@ class AssetId extends Equatable {
};

@override
List<Object?> get props => [id, subClass.formatted];
List<Object?> get props => [id, subClass.formatted, chainId.formattedChainId];

@override
String toString() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ enum CoinSubClass {
smartChain,
moonriver,
ethereumClassic,
tendermintToken,
ubiq,
bep20,
matic,
utxo,
smartBch,
erc20,
tendermint,
tendermintToken,
krc20,
ewt,
hrc20,
Expand Down Expand Up @@ -162,6 +162,25 @@ enum CoinSubClass {
}
}

/// Checks if this subclass can be a parent of the given child subclass
bool canBeParentOf(CoinSubClass child) {
// Tendermint tokens can be a child of Tendermint, but not the
// other way around. This allows Tendermint to be a parent
// while keeping the existing parent subclass check intact.
if (this == CoinSubClass.tendermint &&
child == CoinSubClass.tendermintToken) {
return true;
}

// For most cases, parent and child should have the same subclass
return this == child;
}

/// Checks if this subclass can be a child of the given parent subclass
bool canBeChildOf(CoinSubClass parent) {
return parent.canBeParentOf(this);
}

// TODO: Consider if null or an empty string should be returned for
// subclasses where they don't have a symbol used in coin IDs.
String get formatted {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ abstract class ProtocolClass with ExplorerUrlMixin implements Equatable {
// 'Unsupported protocol type: ${subClass.formatted}',
// ),
};
} catch (e) {
} catch (e, s) {
if (kDebugMode) debugPrintStack(stackTrace: s);
throw ProtocolParsingException(primaryType, e.toString());
}
}
Expand Down
26 changes: 25 additions & 1 deletion packages/komodo_ui/lib/src/defi/asset/asset_icon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ class AssetIcon extends StatelessWidget {

@override
Widget build(BuildContext context) {
final disabledTheme = Theme.of(context).disabledColor;
return Opacity(
opacity: suspended ? 0.4 : 1,
opacity: suspended ? disabledTheme.a : 1.0,
child: SizedBox.square(
dimension: size,
child: _AssetIconResolver(
Expand Down Expand Up @@ -98,6 +99,24 @@ class AssetIcon extends StatelessWidget {
throwExceptions: throwExceptions,
);
}

/// Checks if the asset icon exists in the local assets or CDN **based solely
/// on the internal cache**.
///
/// This method does **not** perform a live check. It only returns `true` if
/// the icon has previously been loaded or pre-cached
/// and its existence has been recorded in the internal `_assetExistenceCache`
/// If the icon has not yet been loaded or pre-cached,
/// this method will return `false` even if the icon actually exists.
///
/// **Note:** The result depends entirely on prior caching or loading attempts
/// To ensure up-to-date results, call [precacheAssetIcon]
/// before using this method.
///
/// Returns true if the icon is known to exist (per cache), false otherwise.
static bool assetIconExists(String assetIconId) {
return _AssetIconResolver.assetIconExists(assetIconId);
}
}

class _AssetIconResolver extends StatelessWidget {
Expand Down Expand Up @@ -203,6 +222,11 @@ class _AssetIconResolver extends StatelessWidget {
}
}

static bool assetIconExists(String assetIconId) {
final resolver = _AssetIconResolver(assetId: assetIconId, size: 20);
return _assetExistenceCache[resolver._imagePath] ?? false;
}

@override
Widget build(BuildContext context) {
if (_customIconsCache.containsKey(_sanitizedId)) {
Expand Down
136 changes: 124 additions & 12 deletions packages/komodo_ui/lib/src/defi/asset/asset_logo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,8 @@ import 'package:komodo_ui/src/defi/asset/asset_icon.dart';
/// Similar to the legacy CoinLogo, but built on top of the new [Asset]
/// and [AssetIcon] APIs.
class AssetLogo extends StatelessWidget {
/// Creates a new [AssetLogo] widget.
/// Creates a new [AssetLogo] widget from an [Asset] instance.
///
/// [asset] is the asset for which the logo should be displayed.
/// [isDisabled] indicates whether the asset is disabled, in which case
/// the icon will be displayed with reduced opacity.
/// [size] is the size of the main asset icon, defaulting to 41 pixels.
/// Example usage:
/// ```dart
/// AssetLogo(asset)
Expand All @@ -23,10 +19,57 @@ class AssetLogo extends StatelessWidget {
this.size = 41,
this.isDisabled = false,
super.key,
});
}) : _assetId = null,
_legacyTicker = null,
isBlank = false;

/// Creates a logo directly from an [AssetId].
const AssetLogo.ofId(
AssetId assetId, {
this.size = 41,
this.isDisabled = false,
super.key,
}) : asset = null,
_assetId = assetId,
_legacyTicker = null,
isBlank = false;

/// Legacy constructor that accepts a raw ticker string.
///
/// This mirrors [AssetIcon.ofTicker] and should only be used when an
/// [Asset] or [AssetId] instance isn't available.
const AssetLogo.ofTicker(
String ticker, {
this.size = 41,
this.isDisabled = false,
super.key,
}) : _legacyTicker = ticker,
asset = null,
_assetId = null,
isBlank = false;

/// Creates a placeholder [AssetLogo] widget.
///
/// This displays the default placeholder icon (monetization_on_outlined)
/// and should be used when no asset data is available or as a fallback.
///
/// Set [isBlank] to true to display an empty circular container instead
/// of the default icon, similar to the legacy placeholder behavior.
const AssetLogo.placeholder({
this.size = 41,
this.isDisabled = false,
this.isBlank = false,
super.key,
}) : asset = null,
_assetId = null,
_legacyTicker = null;

/// Asset to display the logo for.
final Asset asset;
final Asset? asset;

/// AssetId to display the logo for.
final AssetId? _assetId;
final String? _legacyTicker;

/// Size of the main asset icon.
final double size;
Expand All @@ -35,24 +78,93 @@ class AssetLogo extends StatelessWidget {
/// with reduced opacity.
final bool isDisabled;

/// Whether to display a blank placeholder instead of the default icon.
/// Only used with the [AssetLogo.placeholder] constructor.
final bool isBlank;

@override
Widget build(BuildContext context) {
// Existing komodo-wallet implementation doesn't show protocol icon
// for UTXO assets (like BTC, LTC, etc.) so we follow the same logic here.
final protocolTicker = asset.protocol.subClass.iconTicker;
final shouldShowProtocolIcon = asset.protocol.subClass != CoinSubClass.utxo;
final resolvedId = asset?.id ?? _assetId;
final resolvedTicker = _legacyTicker;

// Handle placeholder case
if (resolvedId == null && resolvedTicker == null) {
return _AssetLogoPlaceholder(
isBlank: isBlank,
isDisabled: isDisabled,
size: size,
);
}

final isChildAsset = resolvedId?.isChildAsset ?? false;

// Use the parent coin ticker for child assets so that token logos display
// the network they belong to (e.g. ETH for ERC20 tokens).
final protocolTicker = isChildAsset ? resolvedId?.parentId?.id : null;
final shouldShowProtocolIcon = isChildAsset && protocolTicker != null;

final mainIcon =
resolvedId != null
? AssetIcon(resolvedId, size: size, suspended: isDisabled)
: (resolvedTicker != null
? AssetIcon.ofTicker(
resolvedTicker,
size: size,
suspended: isDisabled,
)
: throw ArgumentError(
'resolvedTicker cannot be null when both asset and '
'assetId are absent.',
));

return Stack(
clipBehavior: Clip.none,
children: [
AssetIcon(asset.id, size: size, suspended: isDisabled),
mainIcon,
if (shouldShowProtocolIcon)
AssetProtocolIcon(protocolTicker: protocolTicker, logoSize: size),
],
);
}
}

class _AssetLogoPlaceholder extends StatelessWidget {
const _AssetLogoPlaceholder({
required this.isBlank,
required this.isDisabled,
required this.size,
});

final bool isBlank;
final bool isDisabled;
final double size;

@override
Widget build(BuildContext context) {
if (isBlank) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color:
isDisabled
? Theme.of(context).disabledColor
: Theme.of(context).colorScheme.secondaryContainer,
),
);
}
return Icon(
Icons.monetization_on_outlined,
size: size,
color:
isDisabled
? Theme.of(context).disabledColor
: Theme.of(context).colorScheme.onSecondaryContainer,
);
}
}

/// A widget that displays a protocol icon with a circular border and shadow,
/// positioned absolutely within its parent widget.
///
Expand Down
Loading