Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export 'coin_config_repository.dart';
export 'coin_config_repository_factory.dart';
export 'coin_config_storage.dart';
export 'config_transform.dart';
export 'custom_token_storage.dart';
export 'custom_token_storage_interface.dart';
export 'github_coin_config_provider.dart';
export 'local_asset_coin_config_provider.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import 'package:hive_ce/hive.dart';
import 'package:komodo_coin_updates/src/coins_config/custom_token_storage_interface.dart';
import 'package:komodo_defi_types/komodo_defi_types.dart';
import 'package:logging/logging.dart';

/// Storage for custom tokens that are not part of the official coin configuration.
/// These tokens are persisted independently from the main coin configuration
/// and are not affected by coin config updates.
class CustomTokenStorage implements ICustomTokenStorage {
/// Creates a custom token storage instance.
/// [customTokensBoxName] is the name of the Hive box for storing custom tokens.
/// [customTokensBox] is an optional pre-opened LazyBox for testing/mocking.
CustomTokenStorage({
this.customTokensBoxName = 'custom_tokens',
LazyBox<Asset>? customTokensBox,
}) : _customTokensBox = customTokensBox;

static final Logger _log = Logger('CustomTokenStorage');

/// The name of the Hive box for custom tokens.
final String customTokensBoxName;

LazyBox<Asset>? _customTokensBox;

@override
Future<void> storeCustomToken(Asset asset) async {
_log.fine('Storing custom token ${asset.id.id}');
final box = await _openCustomTokensBox();
await box.put(asset.id.id, asset);
}

@override
Future<void> storeCustomTokens(List<Asset> assets) async {
_log.fine('Storing ${assets.length} custom tokens');
final box = await _openCustomTokensBox();
final putMap = <String, Asset>{for (final a in assets) a.id.id: a};
await box.putAll(putMap);
}

@override
Future<List<Asset>> getAllCustomTokens() async {
_log.fine('Retrieving all custom tokens');
final box = await _openCustomTokensBox();
final keys = box.keys;
final values = await Future.wait(
keys.map((dynamic key) => box.get(key as String)),
);
return values.whereType<Asset>().toList();
}

@override
Future<Asset?> getCustomToken(AssetId assetId) async {
_log.fine('Retrieving custom token ${assetId.id}');
final box = await _openCustomTokensBox();
return await box.get(assetId.id);
}

@override
Future<bool> hasCustomToken(AssetId assetId) async {
final box = await _openCustomTokensBox();
return box.containsKey(assetId.id);
}

@override
Future<void> deleteCustomToken(AssetId assetId) async {
_log.fine('Deleting custom token ${assetId.id}');
final box = await _openCustomTokensBox();
await box.delete(assetId.id);
}

@override
Future<void> deleteCustomTokens(List<AssetId> assetIds) async {
_log.fine('Deleting ${assetIds.length} custom tokens');
final box = await _openCustomTokensBox();
await box.deleteAll(assetIds.map((id) => id.id));
}

@override
Future<void> deleteAllCustomTokens() async {
_log.fine('Deleting all custom tokens');
final box = await _openCustomTokensBox();
await box.clear();
}

@override
Future<bool> hasCustomTokens() async {
final boxExists = await Hive.boxExists(customTokensBoxName);
if (!boxExists) {
return false;
}

final box = await Hive.openLazyBox<Asset>(customTokensBoxName);
return box.isNotEmpty;
}

@override
Future<bool> updateCustomToken(Asset asset) async {
final box = await _openCustomTokensBox();
final existed = box.containsKey(asset.id.id);
await box.put(asset.id.id, asset);

if (existed) {
_log.fine('Updated existing custom token ${asset.id.id}');
} else {
_log.fine('Stored new custom token ${asset.id.id}');
}

return existed;
}
Comment thread
takenagain marked this conversation as resolved.
Outdated

@override
Future<bool> addCustomTokenIfNotExists(Asset asset) async {
final box = await _openCustomTokensBox();
if (box.containsKey(asset.id.id)) {
_log.fine('Custom token ${asset.id.id} already exists, skipping');
return false;
}

await box.put(asset.id.id, asset);
_log.fine('Added new custom token ${asset.id.id}');
return true;
}

@override
Future<int> getCustomTokenCount() async {
final box = await _openCustomTokensBox();
return box.length;
}

@override
Future<void> dispose() async {
if (_customTokensBox != null) {
_log.fine('Closing custom tokens box');
await _customTokensBox!.close();
_customTokensBox = null;
}
}

Future<LazyBox<Asset>> _openCustomTokensBox() async {
if (_customTokensBox == null) {
_log.fine('Opening custom tokens box "$customTokensBoxName"');
_customTokensBox = await Hive.openLazyBox<Asset>(customTokensBoxName);
}
return _customTokensBox!;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:komodo_defi_types/komodo_defi_types.dart';

/// Interface for custom token storage operations
abstract class ICustomTokenStorage {
/// Stores a single custom token.
/// If a token with the same AssetId already exists, it will be overwritten.
Future<void> storeCustomToken(Asset asset);

/// Stores multiple custom tokens.
/// Existing tokens with the same AssetIds will be overwritten.
Future<void> storeCustomTokens(List<Asset> assets);

/// Retrieves all custom tokens from storage.
/// Returns an empty list if no custom tokens are stored.
Future<List<Asset>> getAllCustomTokens();

/// Retrieves a single custom token by its AssetId.
/// Returns null if the token is not found.
Future<Asset?> getCustomToken(AssetId assetId);

/// Checks if a custom token exists in storage.
Future<bool> hasCustomToken(AssetId assetId);

/// Deletes a single custom token by its AssetId.
Future<void> deleteCustomToken(AssetId assetId);

/// Deletes multiple custom tokens by their AssetIds.
Future<void> deleteCustomTokens(List<AssetId> assetIds);

/// Deletes all custom tokens from storage.
Future<void> deleteAllCustomTokens();

/// Returns true if the custom tokens box exists and is not empty.
Future<bool> hasCustomTokens();

/// Updates an existing custom token if it exists, otherwise stores it as new.
/// Returns true if the token was updated, false if it was newly created.
Future<bool> updateCustomToken(Asset asset);

/// Adds a custom token to storage if it doesn't already exist.
/// Returns true if the token was added, false if it already existed.
Future<bool> addCustomTokenIfNotExists(Asset asset);

/// Returns the number of custom tokens in storage.
Future<int> getCustomTokenCount();

/// Closes the storage and releases resources.
/// This should be called when the storage is no longer needed.
Future<void> dispose();
}
Comment thread
takenagain marked this conversation as resolved.
2 changes: 2 additions & 0 deletions packages/komodo_coins/lib/komodo_coins.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
library komodo_coins;

export 'src/asset_filter.dart';
export 'src/asset_management/coin_config_manager.dart'
show CoinConfigManager, StrategicCoinConfigManager;
export 'src/komodo_asset_update_manager.dart'
show AssetsUpdateManager, KomodoAssetsUpdateManager;
export 'src/startup/startup_coins_provider.dart' show StartupCoinsProvider;
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:collection';

import 'package:komodo_coin_updates/komodo_coin_updates.dart';
import 'package:komodo_coins/src/asset_filter.dart';
import 'package:komodo_coins/src/asset_management/coin_config_fallback_mixin.dart';
import 'package:komodo_coins/src/asset_management/loading_strategy.dart';
Expand Down Expand Up @@ -53,6 +54,12 @@ abstract class CoinConfigManager {

/// Disposes of all resources
Future<void> dispose();

/// Stores a custom token
Future<void> storeCustomToken(Asset asset);

/// Deletes a custom token
Future<void> deleteCustomToken(AssetId assetId);
}

/// Implementation of [CoinConfigManager] that uses strategy pattern for loading
Expand All @@ -63,27 +70,32 @@ class StrategicCoinConfigManager
required List<CoinConfigSource> configSources,
LoadingStrategy? loadingStrategy,
Set<String> defaultPriorityTickers = const {},
ICustomTokenStorage? customTokenStorage,
}) {
return StrategicCoinConfigManager._internal(
configSources: configSources,
loadingStrategy: loadingStrategy ?? StorageFirstLoadingStrategy(),
defaultPriorityTickers: defaultPriorityTickers,
customTokenStorage: customTokenStorage ?? CustomTokenStorage(),
);
}

StrategicCoinConfigManager._internal({
required List<CoinConfigSource> configSources,
required LoadingStrategy loadingStrategy,
required Set<String> defaultPriorityTickers,
required ICustomTokenStorage customTokenStorage,
}) : _configSources = configSources,
_loadingStrategy = loadingStrategy,
_defaultPriorityTickers = Set.unmodifiable(defaultPriorityTickers);
_defaultPriorityTickers = Set.unmodifiable(defaultPriorityTickers),
_customTokenStorage = customTokenStorage;

static final _logger = Logger('StrategicCoinConfigManager');

final List<CoinConfigSource> _configSources;
final LoadingStrategy _loadingStrategy;
final Set<String> _defaultPriorityTickers;
final ICustomTokenStorage _customTokenStorage;

// Required by CoinConfigFallbackMixin
@override
Expand Down Expand Up @@ -185,6 +197,7 @@ class StrategicCoinConfigManager
);

_assets = _mapAssets(assets);
await _loadAndMergeCustomTokens();
_logger.info('Loaded ${assets.length} assets');
}

Expand Down Expand Up @@ -234,6 +247,7 @@ class StrategicCoinConfigManager
);

_assets = _mapAssets(assets);
await _loadAndMergeCustomTokens();
_filterCache.clear(); // Clear cache after refresh

// Refresh commit hash cache when assets are refreshed
Expand Down Expand Up @@ -325,6 +339,97 @@ class StrategicCoinConfigManager
.toSet();
}

/// Creates a duplicate AssetId with a modified name and id to avoid conflicts
AssetId _createDuplicateAssetId(AssetId originalId) {
var counter = 1;
AssetId duplicateId;

do {
final newName = '${originalId.name}_custom$counter';
final newId = '${originalId.id}_custom$counter';

duplicateId = originalId.copyWith(id: newId, name: newName);
counter++;
} while (_assets!.containsKey(duplicateId));

return duplicateId;
}

/// Loads custom tokens and merges them directly into _assets
Future<void> _loadAndMergeCustomTokens() async {
try {
final customTokens = await _customTokenStorage.getAllCustomTokens();
if (customTokens.isEmpty) {
return;
}

// Add custom tokens to _assets, handling conflicts by creating duplicate entries
for (final customToken in customTokens) {
if (_assets!.containsKey(customToken.id)) {
// Conflict detected - create a duplicate entry with a modified name
Comment thread
takenagain marked this conversation as resolved.
Outdated
final duplicateAssetId = _createDuplicateAssetId(customToken.id);
final duplicateAsset = Asset(
id: duplicateAssetId,
protocol: customToken.protocol,
isWalletOnly: customToken.isWalletOnly,
signMessagePrefix: customToken.signMessagePrefix,
);
Comment thread
takenagain marked this conversation as resolved.
Outdated
_assets![duplicateAssetId] = duplicateAsset;
_logger.fine(
'Asset conflict detected for ${customToken.id.id}. '
'Created duplicate with id: ${duplicateAssetId.id}',
);
} else {
_assets![customToken.id] = customToken;
}
}

_logger.fine('Merged ${customTokens.length} custom tokens into assets');
} catch (e, s) {
_logger.warning('Failed to load custom tokens', e, s);
}
}
Comment thread
takenagain marked this conversation as resolved.

@override
Future<void> storeCustomToken(Asset asset) async {
_checkNotDisposed();
_assertInitialized();
await _customTokenStorage.storeCustomToken(asset);
if (_isInitialized) {
// Add the custom token directly to _assets, handling conflicts
if (_assets!.containsKey(asset.id)) {
// Conflict detected - create a duplicate entry with a modified name
Comment thread
takenagain marked this conversation as resolved.
Outdated
final duplicateAssetId = _createDuplicateAssetId(asset.id);
final duplicateAsset = Asset(
id: duplicateAssetId,
protocol: asset.protocol,
isWalletOnly: asset.isWalletOnly,
signMessagePrefix: asset.signMessagePrefix,
);
Comment thread
takenagain marked this conversation as resolved.
Outdated
_assets![duplicateAssetId] = duplicateAsset;
_logger.fine(
'Asset conflict detected for ${asset.id.id}. '
'Created duplicate with id: ${duplicateAssetId.id}',
);
} else {
_assets![asset.id] = asset;
}
_filterCache.clear(); // Clear filter cache after adding custom token
}
}
Comment thread
takenagain marked this conversation as resolved.

@override
Future<void> deleteCustomToken(AssetId assetId) async {
_checkNotDisposed();
_assertInitialized();
await _customTokenStorage.deleteCustomToken(assetId);
if (_isInitialized) {
// Remove the custom token from _assets
_assets!.remove(assetId);
_filterCache.clear(); // Clear filter cache after deleting custom token
}
}
Comment thread
takenagain marked this conversation as resolved.

@override
Future<void> dispose() async {
if (_isDisposed) {
Expand All @@ -336,6 +441,7 @@ class StrategicCoinConfigManager
_assets = null;
_filterCache.clear();
_cachedCommitHash = null; // Clear commit hash cache
await _customTokenStorage.dispose(); // Dispose custom token storage
clearSourceHealthData(); // Clear mixin data
_logger.fine('Disposed StrategicCoinConfigManager');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ class KomodoAssetsUpdateManager implements AssetsUpdateManager {
configSources: configProviders,
loadingStrategy: _loadingStrategy,
defaultPriorityTickers: defaultPriorityTickers,
customTokenStorage: CustomTokenStorage(),
);

// Initialize update manager
Expand Down
Loading
Loading