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
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;
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.

Debug logging is enabled by default in production code. Consider setting this to false by default to prevent performance degradation and log clutter in production.

Suggested change
static bool enableDebugLogging = true;
static bool enableDebugLogging = false;

Copilot uses AI. Check for mistakes.

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;
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.

Debug logging is enabled by default in production code. Consider setting this to false by default to avoid performance overhead and excessive logging in production environments.

Suggested change
static bool enableDebugLogging = true;
static bool enableDebugLogging = false;

Copilot uses AI. Check for mistakes.

@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);
}

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

try {
final response = _rpcCallback(request);
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 response from _rpcCallback is not being awaited. Since executeRpc is declared as async and returns Future<JsonMap>, this synchronous call will cause the stopwatch timing and subsequent logging to execute before the RPC call completes. Change to final response = await _rpcCallback(request);.

Copilot uses AI. Check for mistakes.
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);
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.

Cannot pass response (of type JsonMap) to _logElectrumConnectionInfo which expects JsonMap as its second parameter, but response here is a Future<JsonMap> since line 40 is not awaited. This will cause a type error at runtime.

Copilot uses AI. Check for mistakes.
}

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
17 changes: 17 additions & 0 deletions packages/komodo_defi_sdk/lib/src/balances/balance_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ class BalanceManager implements IBalanceManager {
final KomodoDefiLocalAuth _auth;
StreamSubscription<KdfUser?>? _authSubscription;
final Duration _defaultPollingInterval = const Duration(seconds: 30);

/// Enable debug logging for balance polling
static bool enableDebugLogging = true;
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.

Debug logging is enabled by default in production code. Consider setting this to false by default and allowing it to be enabled via configuration to avoid performance overhead and log noise in production environments.

Suggested change
static bool enableDebugLogging = true;
static bool enableDebugLogging = false;

Copilot uses AI. Check for mistakes.

/// Cache of the latest known balances for each asset
final Map<AssetId, BalanceInfo> _balanceCache = {};
Expand Down Expand Up @@ -351,6 +354,12 @@ class BalanceManager implements IBalanceManager {
return null; // Don't fetch balance if user changed or logged out
}

if (enableDebugLogging) {
_logger.info(
'[POLLING] Fetching balance for ${assetId.name} (every ${_defaultPollingInterval.inSeconds}s)',
);
}

try {
// Ensure asset is activated if needed
final isActive = await _ensureAssetActivated(
Expand All @@ -361,11 +370,19 @@ class BalanceManager implements IBalanceManager {
// Only fetch balance if asset is active
if (isActive) {
final balance = await getBalance(assetId);
if (enableDebugLogging) {
_logger.info(
'[POLLING] Balance fetched for ${assetId.name}: ${balance.total}',
);
}
return balance;
}
} catch (e) {
// Just log the error and continue with the last known balance
// This prevents the stream from terminating on transient errors
if (enableDebugLogging) {
_logger.warning('[POLLING] Balance fetch failed for ${assetId.name}: $e');
}
}

// Return the last known balance if we can't fetch a new one
Expand Down
Loading