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
2 changes: 1 addition & 1 deletion packages/komodo_defi_framework/app_build/build_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"coins": {
"fetch_at_build_enabled": true,
"update_commit_on_build": true,
"bundled_coins_repo_commit": "9d99819f3f7a8357464240c8c26f7442f5a7da32",
"bundled_coins_repo_commit": "3d23cb5dcc82d4bb8c88f8ebf67ad3fb51ed3b6b",
"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",
Expand Down
124 changes: 118 additions & 6 deletions packages/komodo_defi_framework/lib/komodo_defi_framework.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:komodo_defi_framework/src/operations/kdf_operations_factory.dart
import 'package:komodo_defi_framework/src/operations/kdf_operations_interface.dart';
import 'package:komodo_defi_types/komodo_defi_type_utils.dart';
import 'package:komodo_defi_types/komodo_defi_types.dart';
import 'package:logging/logging.dart';

export 'package:komodo_defi_framework/src/client/kdf_api_client.dart';
export 'package:komodo_defi_framework/src/config/kdf_config.dart';
Expand All @@ -32,6 +33,12 @@ class KomodoDefiFramework implements ApiClient {
}
}

/// Enable debug logging for RPC calls (method names, durations, success/failure)
/// This can be controlled via app configuration
static bool enableDebugLogging = true;

final Logger _logger = Logger('KomodoDefiFramework');

factory KomodoDefiFramework.create({
required IKdfHostConfig hostConfig,
void Function(String)? externalLogger,
Expand Down Expand Up @@ -171,15 +178,120 @@ class KomodoDefiFramework implements ApiClient {
return version;
}

/// Checks if KDF is healthy and responsive by attempting a version RPC call.
/// Returns true if KDF is running and responsive, false otherwise.
/// This is useful for detecting when KDF has become unavailable, especially
/// on mobile platforms after app backgrounding.
Future<bool> isHealthy() async {
try {
final isRunningCheck = await isRunning();
if (!isRunningCheck) {
_log('KDF health check failed: not running');
return false;
}

// Additional check: try to get version to verify RPC is responsive
final versionCheck = await version();
if (versionCheck == null) {
_log('KDF health check failed: version call returned null');
return false;
}

_log('KDF health check passed');
return true;
} catch (e) {
_log('KDF health check failed with exception: $e');
return false;
}
}

@override
Future<JsonMap> executeRpc(JsonMap request) async {
final response = (await _kdfOperations.mm2Rpc(
request..setIfAbsentOrEmpty('userpass', _hostConfig.rpcPassword),
)).ensureJson();
if (KdfLoggingConfig.verboseLogging) {
_log('RPC response: ${response.toJsonString()}');
if (!enableDebugLogging) {
final response = (await _kdfOperations.mm2Rpc(
request..setIfAbsentOrEmpty('userpass', _hostConfig.rpcPassword),
)).ensureJson();
if (KdfLoggingConfig.verboseLogging) {
_log('RPC response: ${response.toJsonString()}');
}
return response;
}

// Extract method name for logging
final method = request['method'] as String?;
final stopwatch = Stopwatch()..start();

try {
final response = (await _kdfOperations.mm2Rpc(
request..setIfAbsentOrEmpty('userpass', _hostConfig.rpcPassword),
)).ensureJson();
stopwatch.stop();

_logger.info(
'[RPC] ${method ?? 'unknown'} completed in ${stopwatch.elapsedMilliseconds}ms',
);

// Log electrum-related methods with more detail
if (method != null && _isElectrumRelatedMethod(method)) {
_logger.info('[ELECTRUM] Method: $method, Duration: ${stopwatch.elapsedMilliseconds}ms');
_logElectrumConnectionInfo(method, response);
}

if (KdfLoggingConfig.verboseLogging) {
_log('RPC response: ${response.toJsonString()}');
}
return response;
} catch (e) {
stopwatch.stop();
_logger.warning(
'[RPC] ${method ?? 'unknown'} failed after ${stopwatch.elapsedMilliseconds}ms: $e',
);
rethrow;
}
}

bool _isElectrumRelatedMethod(String method) {
return method.contains('electrum') ||
method.contains('enable') ||
method.contains('utxo') ||
method == 'get_enabled_coins' ||
method == 'my_balance';
}

void _logElectrumConnectionInfo(String method, JsonMap response) {
try {
// Log connection information from enable responses
if (method.contains('enable') && response['result'] != null) {
final result = response['result'] as Map<String, dynamic>?;
if (result != null) {
final address = result['address'] as String?;
final balance = result['balance'] as String?;
_logger.info(
'[ELECTRUM] Coin enabled - Address: ${address ?? 'N/A'}, Balance: ${balance ?? 'N/A'}',
);

// Log server information if available
if (result['servers'] != null) {
final servers = result['servers'];
_logger.info('[ELECTRUM] Connected servers: $servers');
}
}
}

// Log balance information
if (method == 'my_balance' && response['result'] != null) {
final result = response['result'] as Map<String, dynamic>?;
if (result != null) {
final coin = result['coin'] as String?;
final balance = result['balance'] as String?;
_logger.info(
'[ELECTRUM] Balance query - Coin: ${coin ?? 'N/A'}, Balance: ${balance ?? 'N/A'}',
);
}
}
} catch (e) {
// Silently ignore logging errors
}
return response;
}

void _assertHostConfigMatchesStartupConfig(
Expand Down
87 changes: 85 additions & 2 deletions packages/komodo_defi_framework/lib/src/client/kdf_api_client.dart
Original file line number Diff line number Diff line change
@@ -1,24 +1,107 @@
import 'package:komodo_defi_types/komodo_defi_type_utils.dart';
import 'package:komodo_defi_types/komodo_defi_types.dart';
import 'package:logging/logging.dart';

class KdfApiClient implements ApiClient {
KdfApiClient(
this._rpcCallback,
// this.
/*{required String rpcPassword}*/
);
) {
_logger = Logger('KdfApiClient');
}

final JsonMap Function(JsonMap) _rpcCallback;
// final Future<StopStatus> Function() _stopCallback;

// String? _rpcPassword;

late final Logger _logger;

/// Enable debug logging for RPC calls (method names, durations, success/failure)
/// This can be controlled via app configuration
static bool enableDebugLogging = true;

@override
Future<JsonMap> executeRpc(JsonMap request) async {
// if (!await isInitialized()) {
// throw StateError('API client is not initialized');
// }
return _rpcCallback(request);

if (!enableDebugLogging) {
return _rpcCallback(request);
}
Comment on lines +31 to +33
Copy link

Copilot AI Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic is inverted: when debug logging is disabled, the function returns early without logging, but when enabled, it falls through to logging code. However, this means debug logging only works when enableDebugLogging is true, which contradicts the early return. The condition should be if (!enableDebugLogging) with the simple return path, or the logic flow needs restructuring to ensure logging happens when enabled.

Copilot uses AI. Check for mistakes.

// Extract method name for logging
final method = request['method'] as String?;
final stopwatch = Stopwatch()..start();

try {
final response = _rpcCallback(request);
stopwatch.stop();

_logger.info(
'[RPC] ${method ?? 'unknown'} completed in ${stopwatch.elapsedMilliseconds}ms',
);

// Log electrum-related methods with more detail
if (method != null && _isElectrumRelatedMethod(method)) {
_logger.info('[ELECTRUM] Method: $method, Duration: ${stopwatch.elapsedMilliseconds}ms');
_logElectrumConnectionInfo(method, response);
}

return response;
} catch (e) {
stopwatch.stop();
_logger.warning(
'[RPC] ${method ?? 'unknown'} failed after ${stopwatch.elapsedMilliseconds}ms: $e',
);
rethrow;
}
}

bool _isElectrumRelatedMethod(String method) {
return method.contains('electrum') ||
method.contains('enable') ||
method.contains('utxo') ||
method == 'get_enabled_coins' ||
method == 'my_balance';
}

void _logElectrumConnectionInfo(String method, JsonMap response) {
try {
// Log connection information from enable responses
if (method.contains('enable') && response['result'] != null) {
final result = response['result'] as Map<String, dynamic>?;
if (result != null) {
final address = result['address'] as String?;
final balance = result['balance'] as String?;
_logger.info(
'[ELECTRUM] Coin enabled - Address: ${address ?? 'N/A'}, Balance: ${balance ?? 'N/A'}',
);

// Log server information if available
if (result['servers'] != null) {
final servers = result['servers'];
_logger.info('[ELECTRUM] Connected servers: $servers');
}
}
}

// Log balance information
if (method == 'my_balance' && response['result'] != null) {
final result = response['result'] as Map<String, dynamic>?;
if (result != null) {
final coin = result['coin'] as String?;
final balance = result['balance'] as String?;
_logger.info(
'[ELECTRUM] Balance query - Coin: ${coin ?? 'N/A'}, Balance: ${balance ?? 'N/A'}',
);
}
}
} catch (e) {
// Silently ignore logging errors
}
}

// Not sure if this belongs here
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,10 @@ class KdfOperationsNativeLibrary implements IKdfOperations {
Future<bool> isRunning() =>
Future.sync(() => _kdfMainStatus() == MainStatus.rpcIsUp);

final Uri _url = Uri.parse('http://localhost:7783');
// Use 127.0.0.1 instead of localhost to avoid DNS resolution issues on mobile
// platforms, especially after app backgrounding. See:
// https://github.com/KomodoPlatform/komodo-wallet/issues/3213
final Uri _url = Uri.parse('http://127.0.0.1:7783');
final Client _client = Client();

@override
Expand Down
33 changes: 33 additions & 0 deletions packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ abstract interface class IAuthService {
/// Only works if the KDF API is running and the wallet exists
Future<void> restoreSession(KdfUser user);

/// Ensures that KDF is healthy and responsive. If KDF is not healthy,
/// attempts to restart it with the current user's configuration.
/// This is useful for recovering from situations where KDF has become
/// unavailable, especially on mobile platforms after app backgrounding.
/// Returns true if KDF is healthy or was successfully restarted, false otherwise.
Future<bool> ensureKdfHealthy();

Stream<KdfUser?> get authStateChanges;
Future<void> dispose();
}
Expand Down Expand Up @@ -484,4 +491,30 @@ class KdfAuthService implements IAuthService {
}
});
}

@override
Future<bool> ensureKdfHealthy() async {
try {
// First check if KDF is healthy
if (await _kdfFramework.isHealthy()) {
return true;
}

// KDF is not healthy, try to get the current active user
final currentUser = await _getActiveUser();
if (currentUser == null) {
// No current user, just ensure KDF is running in no-auth mode
await _ensureKdfRunning();
return await _kdfFramework.isHealthy();
}

// We have a current user but KDF is not healthy
// Try to restart KDF in no-auth mode first as we don't have the password
await _ensureKdfRunning();
return await _kdfFramework.isHealthy();
} catch (e) {
// Log the error but don't throw - return false to indicate failure
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,13 @@ abstract interface class KomodoDefiAuth {
/// during cancellation.
Future<void> cancelHardwareDeviceInitialization(int taskId);

/// Ensures that KDF is healthy and responsive. If KDF is not healthy,
/// attempts to restart it with the current user's configuration.
/// This is useful for recovering from situations where KDF has become
/// unavailable, especially on mobile platforms after app backgrounding.
/// Returns true if KDF is healthy or was successfully restarted, false otherwise.
Future<bool> ensureKdfHealthy();

/// Disposes of any resources held by the authentication service.
///
/// This method should be called when the authentication service is no longer
Expand Down Expand Up @@ -680,6 +687,12 @@ class KomodoDefiLocalAuth implements KomodoDefiAuth {
}
}

@override
Future<bool> ensureKdfHealthy() async {
await ensureInitialized();
return _authService.ensureKdfHealthy();
}

@override
Future<void> dispose() async {
await _authService.dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ class TrezorAuthService implements IAuthService {
required String password,
}) => _authService.deleteWallet(walletName: walletName, password: password);

@override
Future<bool> ensureKdfHealthy() => _authService.ensureKdfHealthy();

@override
Future<KdfUser> signIn({
required String walletName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class CustomErc20ActivationStrategy extends ProtocolActivationStrategy {
CoinSubClass.hecoChain,
CoinSubClass.rskSmartBitcoin,
CoinSubClass.arbitrum,
CoinSubClass.base,
};

@override
Expand Down Expand Up @@ -94,4 +95,4 @@ class CustomErc20ActivationStrategy extends ProtocolActivationStrategy {
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Erc20ActivationStrategy extends ProtocolActivationStrategy {
CoinSubClass.hecoChain,
CoinSubClass.rskSmartBitcoin,
CoinSubClass.arbitrum,
CoinSubClass.base,
};

@override
Expand Down Expand Up @@ -95,4 +96,4 @@ class Erc20ActivationStrategy extends ProtocolActivationStrategy {
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class EthTaskActivationStrategy extends ProtocolActivationStrategy {
CoinSubClass.hecoChain,
CoinSubClass.rskSmartBitcoin,
CoinSubClass.arbitrum,
CoinSubClass.base,
};

@override
Expand Down
Loading
Loading