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
49 changes: 24 additions & 25 deletions packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ class ActivationManager {
this._assetHistory,
this._customTokenHistory,
this._assetLookup,
this._balanceManager,
);
this._balanceManager, {
required IAssetRefreshNotifier assetRefreshNotifier,
}) : _assetRefreshNotifier = assetRefreshNotifier;

final ApiClient _client;
final KomodoDefiLocalAuth _auth;
final AssetHistoryStorage _assetHistory;
final CustomAssetHistoryStorage _customTokenHistory;
final IAssetLookup _assetLookup;
final IAssetRefreshNotifier _assetRefreshNotifier;
final IBalanceManager _balanceManager;
final _activationMutex = Mutex();
static const _operationTimeout = Duration(seconds: 30);
Expand All @@ -39,12 +41,8 @@ class ActivationManager {
.protect(operation)
.timeout(
_operationTimeout,
onTimeout:
() =>
throw TimeoutException(
'Operation timed out',
_operationTimeout,
),
onTimeout: () =>
throw TimeoutException('Operation timed out', _operationTimeout),
);
}

Expand Down Expand Up @@ -77,13 +75,10 @@ class ActivationManager {
continue;
}

final parentAsset =
group.parentId == null
? null
: _assetLookup.fromId(group.parentId!) ??
(throw StateError(
'Parent asset ${group.parentId} not found',
));
final parentAsset = group.parentId == null
? null
: _assetLookup.fromId(group.parentId!) ??
(throw StateError('Parent asset ${group.parentId} not found'));

yield ActivationProgress(
status: 'Starting activation for ${group.primary.id.name}...',
Expand Down Expand Up @@ -139,14 +134,13 @@ class ActivationManager {
/// Check if asset and its children are already activated
Future<ActivationProgress> _checkActivationStatus(_AssetGroup group) async {
try {
final enabledCoins =
await _client.rpc.generalActivation.getEnabledCoins();
final enabledAssetIds =
enabledCoins.result
.map((coin) => _assetLookup.findAssetsByConfigId(coin.ticker))
.expand((assets) => assets)
.map((asset) => asset.id)
.toSet();
final enabledCoins = await _client.rpc.generalActivation
.getEnabledCoins();
final enabledAssetIds = enabledCoins.result
.map((coin) => _assetLookup.findAssetsByConfigId(coin.ticker))
.expand((assets) => assets)
.map((asset) => asset.id)
.toSet();

final isActive = enabledAssetIds.contains(group.primary.id);
final childrenActive =
Expand Down Expand Up @@ -211,6 +205,11 @@ class ActivationManager {
// Pre-cache balance for the activated asset
await _balanceManager.precacheBalance(asset);
}

// Notify asset manager to refresh custom tokens if any were activated
if (allAssets.any((asset) => asset.protocol.isCustomToken)) {
_assetRefreshNotifier.notifyCustomTokensChanged();
}
}

if (!completer.isCompleted) {
Expand All @@ -237,8 +236,8 @@ class ActivationManager {
}

try {
final enabledCoins =
await _client.rpc.generalActivation.getEnabledCoins();
final enabledCoins = await _client.rpc.generalActivation
.getEnabledCoins();
return enabledCoins.result
.map((coin) => _assetLookup.findAssetsByConfigId(coin.ticker))
.expand((assets) => assets)
Expand Down
7 changes: 7 additions & 0 deletions packages/komodo_defi_sdk/lib/src/assets/asset_lookup.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ abstract class IAssetProvider extends IAssetLookup {
/// Get list of enabled coin tickers
Future<Set<String>> getEnabledCoins();
}

/// Interface for notifying about asset changes that require UI refresh
// ignore: one_member_abstracts
abstract interface class IAssetRefreshNotifier {
/// Notifies that custom tokens have changed and should be refreshed
void notifyCustomTokensChanged();
}
103 changes: 74 additions & 29 deletions packages/komodo_defi_sdk/lib/src/assets/asset_manager.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// lib/src/assets/asset_manager.dart

import 'dart:async';
// TODO: refactor to rely on komodo_coins cache instead of duplicating the
// splaytreemap cache here. This turns it into a thinner wrapper than it
// already is.
import 'dart:async' show StreamSubscription, unawaited;
import 'dart:collection';

import 'package:flutter/foundation.dart' show ValueGetter;
import 'package:flutter/foundation.dart' show ValueGetter, debugPrint;
import 'package:komodo_coins/komodo_coins.dart';
import 'package:komodo_defi_local_auth/komodo_defi_local_auth.dart';
import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart';
Expand Down Expand Up @@ -42,7 +43,7 @@ typedef AssetIdMap = SplayTreeMap<AssetId, Asset>;
///
/// The manager listens to authentication changes to keep the available asset
/// list in sync with the active wallet's capabilities.
class AssetManager implements IAssetProvider {
class AssetManager implements IAssetProvider, IAssetRefreshNotifier {
/// Creates a new instance of AssetManager.
///
/// This is typically created by the SDK and shouldn't need to be instantiated
Expand Down Expand Up @@ -93,7 +94,7 @@ class AssetManager implements IAssetProvider {

_refreshCoins(const NoAssetFilterStrategy());

await _initializeCustomTokens();
await _refreshCustomTokens();
}
Comment thread
takenagain marked this conversation as resolved.

/// Exposes the currently active commit hash for coins config.
Expand All @@ -103,32 +104,50 @@ class AssetManager implements IAssetProvider {
Future<String?> get latestCoinsCommit async => _coins.getLatestCommitHash();

void _refreshCoins(AssetFilterStrategy strategy) {
if (_currentFilterStrategy?.strategyId == strategy.strategyId) return;
_orderedCoins
..clear()
..addAll(_coins.filteredAssets(strategy));
_currentFilterStrategy = strategy;
}

/// Applies a new [strategy] for filtering available assets.
///
/// This is called whenever the authentication state changes so the
/// visible asset list always matches the capabilities of the active wallet.
void setFilterStrategy(AssetFilterStrategy strategy) {
if (_currentFilterStrategy?.strategyId == strategy.strategyId) return;

_currentFilterStrategy = strategy;
if (_coins.isInitialized) {
_refreshCoins(strategy);
// Also refresh custom tokens to apply the new filter strategy
unawaited(_refreshCustomTokens());
Comment thread
takenagain marked this conversation as resolved.
}
}

Future<void> _initializeCustomTokens() async {
Future<void> _refreshCustomTokens() async {
final user = await _auth.currentUser;
if (user != null) {
final customTokens = await _customAssetHistory.getWalletAssets(
user.walletId,
);
for (final customToken in customTokens) {
_orderedCoins[customToken.id] = customToken;
}
if (user == null) {
debugPrint('No user signed in, skipping custom token refresh');
return;
}

// Drop previously injected custom tokens to avoid stale entries
final toRemove = <AssetId>[];
_orderedCoins.forEach((id, asset) {
if (asset.protocol.isCustomToken) toRemove.add(id);
});
for (final id in toRemove) {
_orderedCoins.remove(id);
}

final customTokens = await _customAssetHistory.getWalletAssets(
user.walletId,
);

final filteredCustomTokens = _filterCustomTokens(customTokens);

for (final customToken in filteredCustomTokens) {
_orderedCoins[customToken.id] = customToken;
}
}

Expand All @@ -146,10 +165,9 @@ class AssetManager implements IAssetProvider {
// Trezor does not support all assets yet, so we apply a filter here
// to only show assets that are compatible with Trezor.
// WalletConnect and Metamask will require similar handling in the future.
final strategy =
isTrezor
? const TrezorAssetFilterStrategy(hiddenAssets: {'BCH'})
: const NoAssetFilterStrategy();
final strategy = isTrezor
? const TrezorAssetFilterStrategy(hiddenAssets: {'BCH'})
: const NoAssetFilterStrategy();

setFilterStrategy(strategy);
}
Expand All @@ -159,19 +177,18 @@ class AssetManager implements IAssetProvider {
/// Returns null if no matching asset is found.
/// Throws [StateError] if called before initialization.
@override
Asset? fromId(AssetId id) =>
_coins.isInitialized
? available[id]
: throw StateError(
'Assets have not been initialized. Call init() first.',
);
Asset? fromId(AssetId id) => _coins.isInitialized
? available[id]
: throw StateError(
'Assets have not been initialized. Call init() first.',
);

/// Returns all available assets, ordered by priority.
///
/// Default assets (configured in [KomodoDefiSdkConfig]) appear first,
/// followed by other assets in alphabetical order.
@override
Map<AssetId, Asset> get available => _orderedCoins;
Map<AssetId, Asset> get available => Map.unmodifiable(_orderedCoins);
Map<AssetId, Asset> get availableOrdered => available;

/// Returns currently activated assets for the signed-in user.
Expand Down Expand Up @@ -205,7 +222,9 @@ class AssetManager implements IAssetProvider {
/// ```
@override
Set<Asset> findAssetsByConfigId(String ticker) {
return available.values.where((asset) => asset.id.id == ticker).toSet();
// Create a defensive copy to prevent concurrent modification during iteration
final assetsCopy = List<Asset>.of(_orderedCoins.values);
return assetsCopy.where((asset) => asset.id.id == ticker).toSet();
}

/// Returns child assets for the given parent asset ID.
Expand All @@ -219,7 +238,9 @@ class AssetManager implements IAssetProvider {
/// ```
@override
Set<Asset> childAssetsOf(AssetId parentId) {
return available.values
// Create a defensive copy to prevent concurrent modification during iteration
final assetsCopy = List<Asset>.of(_orderedCoins.values);
return assetsCopy
.where(
(asset) => asset.id.isChildAsset && asset.id.parentId == parentId,
)
Expand Down Expand Up @@ -252,6 +273,30 @@ class AssetManager implements IAssetProvider {
Stream<ActivationProgress> activateAssets(List<Asset> assets) =>
_activationManager().activateAssets(assets);

@override
void notifyCustomTokensChanged() {
// Refresh custom tokens when notified by the activation manager
unawaited(
_refreshCustomTokens().catchError((Object e, StackTrace s) {
debugPrint('Custom token refresh failed: $e');
}),
);
}

/// Filters custom tokens based on the current asset filtering strategy.
///
/// Custom tokens don't have traditional coin configs, so we create a minimal
/// config structure to support filtering decisions. This ensures custom tokens
/// are properly filtered alongside regular assets.
Set<Asset> _filterCustomTokens(Set<Asset> customTokens) {
final strategy = _currentFilterStrategy;
if (strategy == null) return customTokens;

return customTokens.where((Asset token) {
return strategy.shouldInclude(token, token.protocol.config);
}).toSet();
}

/// Disposes of the asset manager, cleaning up resources.
///
/// This is called automatically by the SDK when disposing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ class CustomAssetHistoryStorage {
/// Store custom tokens used by a wallet
Future<void> storeWalletAssets(WalletId walletId, Set<Asset> assets) async {
final key = _getStorageKey(walletId);
final assetsJsonArray = assets.map((asset) => asset.toJson()).toList();
// Use the protocol config instead of asset toJson due to missing fields
// from the incomplete Asset.toJson implementation. Similar to the
// komodo_coin_updates/hive/hive_adapters.dart issue.
final assetsJsonArray = assets
.map((asset) => asset.protocol.config)
.toList();
Comment thread
takenagain marked this conversation as resolved.
Comment thread
takenagain marked this conversation as resolved.
await _storage.write(key: key, value: assetsJsonArray.toJsonString());
}

Expand Down
18 changes: 10 additions & 8 deletions packages/komodo_defi_sdk/lib/src/bootstrap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ Future<void> bootstrap({
container<CustomAssetHistoryStorage>(),
assetManager,
balanceManager,
// Separate interface used to avoid muddying the IAssetProvider interface
assetRefreshNotifier: assetManager,
);

return activationManager;
Expand All @@ -135,8 +137,8 @@ Future<void> bootstrap({
container.registerSingletonAsync<PubkeyManager>(() async {
final client = await container.getAsync<ApiClient>();
final auth = await container.getAsync<KomodoDefiLocalAuth>();
final activationCoordinator =
await container.getAsync<SharedActivationCoordinator>();
final activationCoordinator = await container
.getAsync<SharedActivationCoordinator>();
final pubkeyManager = PubkeyManager(client, auth, activationCoordinator);

// Set the PubkeyManager on BalanceManager now that it's available
Expand Down Expand Up @@ -193,8 +195,8 @@ Future<void> bootstrap({
final auth = await container.getAsync<KomodoDefiLocalAuth>();
final assetProvider = await container.getAsync<AssetManager>();
final pubkeys = await container.getAsync<PubkeyManager>();
final activationCoordinator =
await container.getAsync<SharedActivationCoordinator>();
final activationCoordinator = await container
.getAsync<SharedActivationCoordinator>();
return TransactionHistoryManager(
client,
auth,
Expand All @@ -218,8 +220,8 @@ Future<void> bootstrap({
final assetProvider = await container.getAsync<AssetManager>();
final feeManager = await container.getAsync<FeeManager>();

final activationCoordinator =
await container.getAsync<SharedActivationCoordinator>();
final activationCoordinator = await container
.getAsync<SharedActivationCoordinator>();
return WithdrawalManager(
client,
assetProvider,
Expand All @@ -240,8 +242,8 @@ Future<void> bootstrap({
final client = await container.getAsync<ApiClient>();
final auth = await container.getAsync<KomodoDefiLocalAuth>();
final assetProvider = await container.getAsync<AssetManager>();
final activationCoordinator =
await container.getAsync<SharedActivationCoordinator>();
final activationCoordinator = await container
.getAsync<SharedActivationCoordinator>();
return SecurityManager(
client,
auth,
Expand Down
Loading
Loading