diff --git a/packages/komodo_coin_updates/lib/src/coins_config/coin_config_repository.dart b/packages/komodo_coin_updates/lib/src/coins_config/coin_config_repository.dart index 35c2c9b2..71d21121 100644 --- a/packages/komodo_coin_updates/lib/src/coins_config/coin_config_repository.dart +++ b/packages/komodo_coin_updates/lib/src/coins_config/coin_config_repository.dart @@ -1,8 +1,5 @@ import 'package:hive_ce/hive.dart'; -import 'package:komodo_coin_updates/src/coins_config/coin_config_provider.dart'; -import 'package:komodo_coin_updates/src/coins_config/coin_config_storage.dart'; -import 'package:komodo_coin_updates/src/coins_config/config_transform.dart'; -import 'package:komodo_coin_updates/src/coins_config/github_coin_config_provider.dart'; +import 'package:komodo_coin_updates/src/coins_config/_coins_config_index.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:logging/logging.dart'; @@ -19,7 +16,8 @@ class CoinConfigRepository implements CoinConfigStorage { this.assetsBoxName = 'assets', this.settingsBoxName = 'coins_settings', this.coinsCommitKey = 'coins_commit', - }); + AssetParser assetParser = const AssetParser(), + }) : _assetParser = assetParser; /// Convenience factory that derives a provider from a runtime config and /// uses default Hive boxes (`assets`, `coins_settings`). @@ -30,11 +28,13 @@ class CoinConfigRepository implements CoinConfigStorage { this.assetsBoxName = 'assets', this.settingsBoxName = 'coins_settings', this.coinsCommitKey = 'coins_commit', + AssetParser assetParser = const AssetParser(), }) : coinConfigProvider = GithubCoinConfigProvider.fromConfig( config, githubToken: githubToken, transformer: transformer, - ); + ), + _assetParser = assetParser; static final Logger _log = Logger('CoinConfigRepository'); /// The provider that fetches the coins and coin configs. @@ -52,6 +52,8 @@ class CoinConfigRepository implements CoinConfigStorage { /// The key for the coins commit. The value is the commit hash. final String coinsCommitKey; + final AssetParser _assetParser; + /// Fetches the latest commit from the provider, downloads assets for that /// commit, and upserts them in local storage along with the commit hash. /// Throws an [Exception] if the request fails at any step. @@ -101,6 +103,11 @@ class CoinConfigRepository implements CoinConfigStorage { @override /// Retrieves all assets from storage, excluding any whose symbol appears /// in [excludedAssets]. Returns an empty list if storage is empty. + /// + /// This method implements a two-pass parsing strategy to rebuild parent-child + /// relationships between assets, similar to AssetParser: + /// 1. First pass: Parse platform assets (no parent) to get their AssetIds + /// 2. Second pass: Reparse child assets using known platform AssetIds Future> getAssets({ List excludedAssets = const [], }) async { @@ -112,16 +119,25 @@ class CoinConfigRepository implements CoinConfigStorage { final values = await Future.wait( keys.map((dynamic key) => box.get(key as String)), ); - final result = values + final allAssetConfigs = values .whereType() .where((a) => !excludedAssets.contains(a.id.id)) - .toList(); - _log.fine('Retrieved ${result.length} assets'); - return result; + .map( + (asset) => + MapEntry(asset.id.symbol.assetConfigId, asset.protocol.config), + ); + + final transformedConfigs = Map>.fromEntries( + allAssetConfigs, + ); + return _assetParser.parseAssetsFromConfig(transformedConfigs); } @override /// Retrieves a single [Asset] by its [assetId] from storage. + /// NOTE: Parent/child relationships are not rebuilt for single asset retrieval. + /// Use [getAssets] if you need proper parent relationships. + /// Returns `null` if the asset is not found. Future getAsset(AssetId assetId) async { _log.fine('Retrieving asset ${assetId.id}'); final a = await (await _openAssetsBox()).get(assetId.id); diff --git a/packages/komodo_coins/lib/src/startup/startup_coins_provider.dart b/packages/komodo_coins/lib/src/startup/startup_coins_provider.dart index 68099d6f..b8b4c366 100644 --- a/packages/komodo_coins/lib/src/startup/startup_coins_provider.dart +++ b/packages/komodo_coins/lib/src/startup/startup_coins_provider.dart @@ -75,9 +75,15 @@ class StartupCoinsProvider { await manager.init(); final assets = manager.all; - final configs = [ - for (final asset in assets.values) asset.protocol.config, - ]; + // Sort to avoid random ordering of params that causes segfault on linux + final configs = + [for (final asset in assets.values) asset.protocol.config] + ..sort((a, b) { + final aId = a['coin'] as String? ?? ''; + final bId = b['coin'] as String? ?? ''; + return aId.compareTo(bId); + }); + return JsonList.of(configs); } finally { try { diff --git a/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart b/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart index 1c56da98..91b4b850 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart @@ -192,24 +192,43 @@ class ActivationManager { if (progress.isSuccess) { final user = await _auth.currentUser; if (user != null) { - await _assetHistory.addAssetToWallet( - user.walletId, - group.primary.id.id, - ); + // TODO: consider abstracting this and other custom token operations out + // of the activation manager + if (group.primary.protocol.isCustomToken) { + await _customTokenHistory.addAssetToWallet( + user.walletId, + group.primary, + _assetLookup.available.keys.toSet(), + ); + } else { + await _assetHistory.addAssetToWallet( + user.walletId, + group.primary.id.id, + ); + } final allAssets = [group.primary, ...(group.children?.toList() ?? [])]; + + // Wait for asset refresh to complete before precaching balances to ensure + // custom token is available for balance precaching. This prevents race + // conditions where balance precaching fails because the custom token + // isn't yet available in the asset lookup. + if (allAssets.any((asset) => asset.protocol.isCustomToken)) { + await _assetRefreshNotifier.notifyAndWaitForCustomTokensRefresh(); + } + for (final asset in allAssets) { if (asset.protocol.isCustomToken) { - await _customTokenHistory.addAssetToWallet(user.walletId, asset); + await _customTokenHistory.addAssetToWallet( + user.walletId, + asset, + _assetLookup.available.keys.toSet(), + ); } + // 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) { diff --git a/packages/komodo_defi_sdk/lib/src/assets/asset_lookup.dart b/packages/komodo_defi_sdk/lib/src/assets/asset_lookup.dart index abf0ad63..312ec072 100644 --- a/packages/komodo_defi_sdk/lib/src/assets/asset_lookup.dart +++ b/packages/komodo_defi_sdk/lib/src/assets/asset_lookup.dart @@ -27,8 +27,10 @@ abstract class IAssetProvider extends IAssetLookup { } /// 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(); + + /// Notifies that custom tokens have changed and waits for refresh to complete + Future notifyAndWaitForCustomTokensRefresh(); } diff --git a/packages/komodo_defi_sdk/lib/src/assets/asset_manager.dart b/packages/komodo_defi_sdk/lib/src/assets/asset_manager.dart index 2be1e544..9b0ee64c 100644 --- a/packages/komodo_defi_sdk/lib/src/assets/asset_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/assets/asset_manager.dart @@ -142,6 +142,7 @@ class AssetManager implements IAssetProvider, IAssetRefreshNotifier { final customTokens = await _customAssetHistory.getWalletAssets( user.walletId, + _orderedCoins.keys.toSet(), ); final filteredCustomTokens = _filterCustomTokens(customTokens); @@ -283,6 +284,16 @@ class AssetManager implements IAssetProvider, IAssetRefreshNotifier { ); } + @override + Future notifyAndWaitForCustomTokensRefresh() async { + try { + await _refreshCustomTokens(); + } catch (e) { + debugPrint('Custom token refresh failed: $e'); + rethrow; + } + } + /// Filters custom tokens based on the current asset filtering strategy. /// /// Custom tokens don't have traditional coin configs, so we create a minimal diff --git a/packages/komodo_defi_sdk/lib/src/assets/custom_asset_history_storage.dart b/packages/komodo_defi_sdk/lib/src/assets/custom_asset_history_storage.dart index b556860a..dceb3c81 100644 --- a/packages/komodo_defi_sdk/lib/src/assets/custom_asset_history_storage.dart +++ b/packages/komodo_defi_sdk/lib/src/assets/custom_asset_history_storage.dart @@ -21,9 +21,16 @@ class CustomAssetHistoryStorage { } /// Add a single asset to wallet's history - Future addAssetToWallet(WalletId walletId, Asset asset) async { - final assets = await getWalletAssets(walletId); - // Equatable operators not working as expected, so we need to check manually + /// + /// [walletId] is the wallet to add the asset to. + /// [asset] is the asset to add to the wallet. + /// [knownAssets] is used to find the parent asset for child assets. + Future addAssetToWallet( + WalletId walletId, + Asset asset, + Set knownAssets, + ) async { + final assets = await getWalletAssets(walletId, knownAssets); if (assets.any((historicalAsset) => historicalAsset.id.id == asset.id.id)) { return; } @@ -32,15 +39,25 @@ class CustomAssetHistoryStorage { } /// Get all assets previously used by a wallet - Future> getWalletAssets(WalletId walletId) async { + /// + /// [walletId] is the wallet to get the assets from. + /// [knownAssets] is used to find the parent asset for child assets. + Future> getWalletAssets( + WalletId walletId, + Set knownAssets, + ) async { final key = _getStorageKey(walletId); final value = await _storage.read(key: key); if (value == null || value.isEmpty) return {}; final assetsJsonArray = jsonListFromString(value); - return assetsJsonArray.map(Asset.fromJson).toSet(); + return assetsJsonArray + .map((json) => Asset.fromJson(json, knownIds: knownAssets)) + .toSet(); } /// Clear wallet's custom token history + /// + /// [walletId] is the wallet to clear the assets from. Future clearWalletAssets(WalletId walletId) async { final key = _getStorageKey(walletId); await _storage.delete(key: key); diff --git a/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/etherscan_transaction_history_strategy.dart b/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/etherscan_transaction_history_strategy.dart index 0bcd566c..cf407af9 100644 --- a/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/etherscan_transaction_history_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/etherscan_transaction_history_strategy.dart @@ -1,5 +1,5 @@ import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' show kDebugMode; import 'package:http/http.dart' as http; import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_sdk/src/pubkeys/pubkey_manager.dart'; @@ -63,8 +63,9 @@ class EtherscanTransactionStrategy extends TransactionHistoryStrategy { for (final address in addresses) { final uri = url.replace( pathSegments: [...url.pathSegments, address.address], - queryParameters: - asset.protocol.isTestnet ? {'testnet': 'true'} : null, + queryParameters: asset.protocol.isTestnet + ? {'testnet': 'true'} + : null, ); // Add the address as the next path segment @@ -88,14 +89,14 @@ class EtherscanTransactionStrategy extends TransactionHistoryStrategy { t.fromId, t.itemCount, ), - _ => - throw UnsupportedError( - 'Unsupported pagination type: ${pagination.runtimeType}', - ), + _ => throw UnsupportedError( + 'Unsupported pagination type: ${pagination.runtimeType}', + ), }; - final currentBlock = - allTransactions.isNotEmpty ? allTransactions.first.blockHeight : 0; + final currentBlock = allTransactions.isNotEmpty + ? allTransactions.first.blockHeight + : 0; return MyTxHistoryResponse( mmrpc: RpcVersion.v2_0, @@ -152,13 +153,12 @@ class EtherscanTransactionStrategy extends TransactionHistoryStrategy { blockHeight: tx.value('block_height'), confirmations: tx.value('confirmations'), timestamp: tx.value('timestamp'), - feeDetails: - tx.valueOrNull('fee_details') != null - ? FeeInfo.fromJson( - tx.value('fee_details') - ..setIfAbsentOrEmpty('type', 'EthGas'), - ) - : null, + feeDetails: tx.valueOrNull('fee_details') != null + ? FeeInfo.fromJson( + tx.value('fee_details') + ..setIfAbsentOrEmpty('type', 'EthGas'), + ) + : null, coin: coinId, internalId: tx.value('internal_id'), memo: tx.valueOrNull('memo'), @@ -252,7 +252,12 @@ class EtherscanProtocolHelper { } final protocol = asset.protocol as Erc20Protocol; - return '$baseEndpoint/${protocol.swapContractAddress}'; + final tokenContractAddress = protocol.contractAddress; + if (tokenContractAddress == null || tokenContractAddress.isEmpty) { + return null; + } + + return '$baseEndpoint/$tokenContractAddress'; } String? _getBaseEndpoint(AssetId id) {