diff --git a/packages/komodo_coin_updates/lib/src/coins_config/config_transform.dart b/packages/komodo_coin_updates/lib/src/coins_config/config_transform.dart index c550fbee3..df310d245 100644 --- a/packages/komodo_coin_updates/lib/src/coins_config/config_transform.dart +++ b/packages/komodo_coin_updates/lib/src/coins_config/config_transform.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart' show kIsWeb, kIsWasm; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:logging/logging.dart'; /// Defines a transform that can be applied to a single coin configuration. /// @@ -29,6 +30,7 @@ class CoinConfigTransformer { transforms ?? const [ WssWebsocketTransform(), + SslElectrumTransform(), ZhtlcLightWalletTransform(), ParentCoinTransform(), ]; @@ -205,6 +207,121 @@ class WssWebsocketTransform implements CoinConfigTransform { /// Specifies which type of Electrum servers to retain enum ElectrumServerType { wssOnly, nonWssOnly } +/// Filters out insecure connections on non-web platforms, emulating the +/// filtering applied to the coins_config_ssl.json file in KomodoPlatform/coins +/// On non-web platforms, only SSL electrum servers and HTTPS URLs are +/// supported for security. +// TODO: move this to a build-time step via the coins config and/or the +// komodo_wallet_build_transformer package +class SslElectrumTransform implements CoinConfigTransform { + const SslElectrumTransform(); + + static final _log = Logger('SslElectrumTransform'); + + @override + /// Determines if the transform should run by checking if this is a non-web + /// platform with any of the filterable fields in the configuration. + bool needsTransform(JsonMap config) { + // Only run on non-web platforms + if (kIsWeb || kIsWasm) return false; + + final electrum = config.valueOrNull('electrum'); + final rpcNodes = config.valueOrNull('nodes'); + final lightWalletServers = config.valueOrNull( + 'light_wallet_d_servers', + ); + + return electrum != null || rpcNodes != null || lightWalletServers != null; + } + + @override + /// Filters entries to keep only secure connections on non-web platforms. + JsonMap transform(JsonMap config) { + final result = JsonMap.of(config); + final coin = config.valueOrNull('coin') ?? 'unknown'; + // Filter electrum servers - keep only SSL protocol + final electrum = config.valueOrNull('electrum'); + if (electrum != null) { + final originalCount = electrum.length; + final filteredElectrums = electrum.where((JsonMap e) { + final protocol = e.valueOrNull('protocol'); + return protocol == 'SSL'; + }).toList(); + + if (filteredElectrums.isEmpty && originalCount > 0) { + _log.warning( + 'SslElectrumTransform: All $originalCount electrum servers filtered ' + 'out for $coin (no SSL servers available)', + ); + } + + result['electrum'] = filteredElectrums; + } + + // Filter RPC nodes - keep only HTTPS URLs + final rpcNodes = config.valueOrNull('nodes'); + if (rpcNodes != null) { + final originalCount = rpcNodes.length; + final filteredRpcNodes = rpcNodes.where((JsonMap node) { + final url = node.valueOrNull('url'); + return url != null && url.startsWith('https://'); + }).toList(); + + if (filteredRpcNodes.isEmpty && originalCount > 0) { + _log.warning( + 'SslElectrumTransform: All $originalCount RPC nodes filtered ' + 'out for $coin (no HTTPS nodes available)', + ); + } + + result['nodes'] = filteredRpcNodes; + } + + // Filter light wallet servers - keep only HTTPS URLs + final lightWalletServers = config.valueOrNull( + 'light_wallet_d_servers', + ); + if (lightWalletServers != null) { + final originalCount = lightWalletServers.length; + final filteredLightWalletServers = lightWalletServers.where(( + dynamic server, + ) { + return server is String && server.startsWith('https://'); + }).toList(); + + if (filteredLightWalletServers.isEmpty && originalCount > 0) { + _log.warning( + 'SslElectrumTransform: All $originalCount light wallet servers ' + 'filtered out for $coin (no HTTPS servers available)', + ); + } + + result['light_wallet_d_servers'] = filteredLightWalletServers; + } + + // Mark coin as having insufficient secure servers if all critical servers were filtered + final hasElectrum = (result['electrum'] as List?)?.isNotEmpty ?? false; + final hasNodes = (result['nodes'] as List?)?.isNotEmpty ?? false; + final hasLightWallet = + (result['light_wallet_d_servers'] as List?)?.isNotEmpty ?? false; + + // If the coin had servers but now has none, mark it + if (!hasElectrum && + !hasNodes && + !hasLightWallet && + (electrum != null || rpcNodes != null || lightWalletServers != null)) { + _log.severe( + 'SslElectrumTransform: Coin $coin has no secure servers available ' + 'after filtering - coin may not be activatable', + ); + // Optionally mark the coin for exclusion or special handling + // result['_ssl_filtered_empty'] = true; + } + + return result; + } +} + class ParentCoinTransform implements CoinConfigTransform { const ParentCoinTransform(); diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index 1c19bff50..921a7f8ba 100644 --- a/packages/komodo_defi_framework/app_build/build_config.json +++ b/packages/komodo_defi_framework/app_build/build_config.json @@ -1,26 +1,25 @@ { "api": { - "api_commit_hash": "9aa41b4c741907d59e4887db08cf84fb78e967e0", + "api_commit_hash": "1874d4399fc8c084108e62eb781e3300d2606213", "branch": "dev", - "fetch_at_build_enabled": false, + "fetch_at_build_enabled": true, "concurrent_downloads_enabled": true, "source_urls": [ "https://api.github.com/repos/KomodoPlatform/komodo-defi-framework", - "https://sdk.devbuilds.komodo.earth/", - "https://nebula.decker.im/" + "https://sdk.devbuilds.komodo.earth/" ], "platforms": { "web": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-wasm|mm2_[a-f0-9]{7,40}-wasm|mm2-[a-f0-9]{7,40}-wasm)\\.zip$", "valid_zip_sha256_checksums": [ - "f92d61595317c16b8f8294c038c7c9215aca94187e22aedc7e0adeba93c94d82" + "267de38f10e2f0a545813547cc4dd0a7160ba42da2e02cdc4391d4d4ea660526" ], "path": "web/kdf/bin" }, "ios": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-ios-aarch64|mm2_[a-f0-9]{7,40}-ios-aarch64|mm2-[a-f0-9]{7,40}-ios-aarch64-CI)\\.zip$", "valid_zip_sha256_checksums": [ - "4f18e9e82ca16e7b1133cabd898d7503f925be1b7d06693f687c34866617da2e" + "32b49d10acf514884d4d599bc24f12abad0f720735fe22ab19602305fde284e4" ], "path": "ios" }, @@ -31,35 +30,35 @@ "mac-arm64" ], "valid_zip_sha256_checksums": [ - "169db9b1410888d7da9f6a0ead2e1a05834528982cebbb70a2e2e3d01e220b69" + "99c3e3820b5a81c9e3153c4538364ea12835806b24a66b90bd6944848c46602d" ], "path": "macos/bin" }, "windows": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-win-x86-64|mm2_[a-f0-9]{7,40}-win-x86-64|mm2-[a-f0-9]{7,40}-Win64)\\.zip$", "valid_zip_sha256_checksums": [ - "d4f5954071df5c2c23016a69074bf19d3daeba10ab413fda2fce8205ee32184c" + "e16eb75012ffec284c865cf9eef6e4c58247227a26af0fd2e290af35342cfdd7" ], "path": "windows/bin" }, "android-armv7": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-android-armv7|mm2_[a-f0-9]{7,40}-android-armv7|mm2-[a-f0-9]{7,40}-android-armv7-CI)\\.zip$", "valid_zip_sha256_checksums": [ - "d964f608787683de67ddde7d827dee88185509cc5ffeb6e85d5f7e2aeba84f08" + "730d1247a1f85351cfe923b8b8c58f50b54f88062f713564d63ddc9298f8cb39" ], "path": "android/app/src/main/cpp/libs/armeabi-v7a" }, "android-aarch64": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-android-aarch64|mm2_[a-f0-9]{7,40}-android-aarch64|mm2-[a-f0-9]{7,40}-android-aarch64-CI)\\.zip$", "valid_zip_sha256_checksums": [ - "32927dfa36a283d344c34cd1514ca8ea3233f20752ec15b7929309cf4df969c2" + "4c5307ccdf982780498192c28e110715f2aa05f95666e864065577e2656397f6" ], "path": "android/app/src/main/cpp/libs/arm64-v8a" }, "linux": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-linux-x86-64|mm2_[a-f0-9]{7,40}-linux-x86-64|mm2-[a-f0-9]{7,40}-Linux-Release)\\.zip$", "valid_zip_sha256_checksums": [ - "e3315a46cb9e1957206b36036954bce51d61d8c4784ed878cc93360b9d01c1cb" + "983f652d6a6d0f77924bdda4f0539aea51b46c2f4c07a1aecc5eec1569e5a160" ], "path": "linux/bin" } @@ -68,7 +67,7 @@ "coins": { "fetch_at_build_enabled": true, "update_commit_on_build": true, - "bundled_coins_repo_commit": "5344c5ab44a153020ea2bb3893a752f19df927d0", + "bundled_coins_repo_commit": "dbc5950f1df9c35e327c8b77d54957c277a6fb26", "coins_repo_api_url": "https://api.github.com/repos/KomodoPlatform/coins", "coins_repo_content_url": "https://raw.githubusercontent.com/KomodoPlatform/coins", "coins_repo_branch": "master", diff --git a/packages/komodo_defi_framework/lib/src/operations/kdf_operations_remote.dart b/packages/komodo_defi_framework/lib/src/operations/kdf_operations_remote.dart index 8d934ce27..ed8a61e90 100644 --- a/packages/komodo_defi_framework/lib/src/operations/kdf_operations_remote.dart +++ b/packages/komodo_defi_framework/lib/src/operations/kdf_operations_remote.dart @@ -37,8 +37,11 @@ class KdfOperationsRemote implements IKdfOperations { // If the scheme is not provided, default to http url = url.scheme.isEmpty ? url.replace(scheme: 'http') : url; - // If it's localhost, return the url as is - if (url.host.contains('localhost') || url.host.contains('127.0.0.1')) { + // Normalize localhost to 127.0.0.1 to avoid DNS resolution issues on Android + if (url.host == 'localhost') { + return url.replace(host: '127.0.0.1'); + } + if (url.host == '127.0.0.1') { return url; } 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 45060eff0..632a38cc2 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart @@ -108,6 +108,7 @@ class ActivationManager { _client, privKeyPolicy, _configService, + _activatedAssetsCache, ); await for (final progress in activator.activate( @@ -139,13 +140,8 @@ class ActivationManager { /// Check if asset and its children are already activated Future _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(); + // Use cache instead of direct RPC call to avoid excessive requests + final enabledAssetIds = await _activatedAssetsCache.getActivatedAssetIds(); final isActive = enabledAssetIds.contains(group.primary.id); final childrenActive = diff --git a/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_base.dart b/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_base.dart index 817fc0f40..3343e94ac 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_base.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_base.dart @@ -1,3 +1,4 @@ +import 'package:komodo_defi_sdk/src/assets/activated_assets_cache.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; abstract class AssetActivator { @@ -23,9 +24,14 @@ abstract class BatchCapableActivator extends AssetActivator { /// Smart activator that chooses between batch/single methods class SmartAssetActivator extends BatchCapableActivator { - SmartAssetActivator(super.client, this._activator); + SmartAssetActivator( + super.client, + this._activator, + this._activatedAssetsCache, + ); final CompositeAssetActivator _activator; + final ActivatedAssetsCache _activatedAssetsCache; @override bool get supportsBatchActivation => true; @@ -75,8 +81,9 @@ class SmartAssetActivator extends BatchCapableActivator { } Future _isAssetActive(Asset asset) async { - final enabledCoins = await client.rpc.generalActivation.getEnabledCoins(); - return enabledCoins.result.any((coin) => coin.ticker == asset.id.id); + // Use cache instead of direct RPC call to avoid excessive requests + final enabledAssetIds = await _activatedAssetsCache.getActivatedAssetIds(); + return enabledAssetIds.contains(asset.id); } bool _supportsBatchActivation(Asset asset) { diff --git a/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_factory.dart b/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_factory.dart index d16ed8257..9c552b0ac 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_factory.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_factory.dart @@ -1,6 +1,7 @@ import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_sdk/src/activation/_activation.dart'; import 'package:komodo_defi_sdk/src/activation_config/activation_config_service.dart'; +import 'package:komodo_defi_sdk/src/assets/activated_assets_cache.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; /// Factory for creating the complete activation strategy stack @@ -11,10 +12,12 @@ class ActivationStrategyFactory { /// [privKeyPolicy] The [PrivateKeyPolicy] to use for private key management. /// This is used for external wallet support. E.g. trezor, wallet connect, etc /// [configService] The [ActivationConfigService] for resolving activation configuration. + /// [activatedAssetsCache] The [ActivatedAssetsCache] to use for checking activation status. static SmartAssetActivator createStrategy( ApiClient client, PrivateKeyPolicy privKeyPolicy, ActivationConfigService configService, + ActivatedAssetsCache activatedAssetsCache, ) { return SmartAssetActivator( client, @@ -34,6 +37,7 @@ class ActivationStrategyFactory { ZhtlcActivationStrategy(client, privKeyPolicy, configService), CustomErc20ActivationStrategy(client), ]), + activatedAssetsCache, ); } } diff --git a/packages/komodo_defi_sdk/lib/src/sdk/komodo_defi_sdk_config.dart b/packages/komodo_defi_sdk/lib/src/sdk/komodo_defi_sdk_config.dart index 5792aeaa1..14c00170d 100644 --- a/packages/komodo_defi_sdk/lib/src/sdk/komodo_defi_sdk_config.dart +++ b/packages/komodo_defi_sdk/lib/src/sdk/komodo_defi_sdk_config.dart @@ -9,7 +9,7 @@ class KomodoDefiSdkConfig { this.preActivateCustomTokenAssets = true, this.maxPreActivationAttempts = 3, this.activationRetryDelay = const Duration(seconds: 2), - this.activatedAssetsCacheTtl = const Duration(seconds: 2), + this.activatedAssetsCacheTtl = const Duration(seconds: 10), this.marketDataConfig = const MarketDataConfig(), });