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
69 changes: 56 additions & 13 deletions packages/komodo_defi_framework/lib/komodo_defi_framework.dart
Original file line number Diff line number Diff line change
Expand Up @@ -173,38 +173,53 @@ class KomodoDefiFramework implements ApiClient {
}

Future<String?> version() async {
final version = await _kdfOperations.version();
_log('KDF version: $version');
return version;
final stopwatch = Stopwatch()..start();
_log('version(): Starting version RPC call via ${_kdfOperations.operationsName}');
try {
final version = await _kdfOperations.version();
stopwatch.stop();
_log('version(): Completed in ${stopwatch.elapsedMilliseconds}ms, result=$version');
return version;
} catch (e) {
stopwatch.stop();
_log('version(): Failed after ${stopwatch.elapsedMilliseconds}ms with error: $e');
rethrow;
}
}

/// 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.
///
/// IMPORTANT: This method ONLY relies on actual RPC verification (version() call)
/// to avoid false positives where native status reports "running" but HTTP listener
/// is not accepting connections (common after iOS 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
// Only rely on actual RPC verification - don't trust native status alone
final versionCheck = await version();
if (versionCheck == null) {
_log('KDF health check failed: version call returned null');
return false;
}

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

/// Resets the HTTP client to drop stale keep-alive connections.
/// This is useful after KDF has been killed and restarted to ensure
/// we don't try to reuse dead connections.
void resetHttpClient() {
_log('Resetting HTTP client to drop stale connections');
_kdfOperations.resetHttpClient();
}

@override
Future<JsonMap> executeRpc(JsonMap request) async {
if (!enableDebugLogging) {
Expand Down Expand Up @@ -248,9 +263,37 @@ class KomodoDefiFramework implements ApiClient {
return response;
} catch (e) {
stopwatch.stop();
_logger.warning(
'[RPC] ${method ?? 'unknown'} failed after ${stopwatch.elapsedMilliseconds}ms: $e',

// Detect transport-fatal SocketExceptions that indicate KDF is down/dying
// errno 32 (EPIPE): Broken pipe - writing to socket whose peer closed
// errno 54 (ECONNRESET): Connection reset by peer
// errno 60 (ETIMEDOUT): Operation timed out
// errno 61 (ECONNREFUSED): Connection refused - no listener on port
final errorString = e.toString().toLowerCase();
final isSocketException = errorString.contains('socketexception');
final isFatalTransportError = isSocketException && (
errorString.contains('broken pipe') || errorString.contains('errno = 32') ||
errorString.contains('connection reset') || errorString.contains('errno = 54') ||
errorString.contains('operation timed out') || errorString.contains('errno = 60') ||
errorString.contains('connection refused') || errorString.contains('errno = 61')
);

if (isFatalTransportError) {
final errorType = errorString.contains('errno = 32') || errorString.contains('broken pipe') ? 'EPIPE (32)' :
errorString.contains('errno = 54') || errorString.contains('connection reset') ? 'ECONNRESET (54)' :
errorString.contains('errno = 60') || errorString.contains('operation timed out') ? 'ETIMEDOUT (60)' :
'ECONNREFUSED (61)';
Comment on lines +282 to +285
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

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

The nested ternary operator chain for error type mapping is difficult to read and maintain. Consider refactoring to use a helper function or if-else statements for better clarity.

Copilot uses AI. Check for mistakes.
_logger.severe(
'[RPC] ${method ?? 'unknown'} failed: KDF transport error $errorType. '
'Resetting HTTP client to drop stale connections.',
);
// Reset HTTP client immediately to drop stale keep-alive connections
resetHttpClient();
} else {
_logger.warning(
'[RPC] ${method ?? 'unknown'} failed after ${stopwatch.elapsedMilliseconds}ms: $e',
);
}
rethrow;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ abstract interface class IKdfOperations {
/// concerns.
Future<bool> isAvailable(IKdfHostConfig hostConfig);

/// Resets the HTTP client to drop stale keep-alive connections.
/// This is useful after KDF has been killed and restarted to ensure
/// we don't try to reuse dead connections.
void resetHttpClient();

/// Dispose of any resources used by this operations implementation
void dispose();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,12 @@ class KdfOperationsLocalExecutable implements IKdfOperations {
}
}

@override
void resetHttpClient() {
// Delegate to remote operations
_kdfRemote.resetHttpClient();
}

@override
void dispose() {
// Cancel and clean up subscriptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ class KdfOperationsNativeLibrary implements IKdfOperations {
// 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();
Client _client = Client();

@override
Future<Map<String, dynamic>> mm2Rpc(Map<String, dynamic> request) async {
Expand Down Expand Up @@ -297,6 +297,13 @@ class KdfOperationsNativeLibrary implements IKdfOperations {
}
}

@override
void resetHttpClient() {
_log('Resetting HTTP client to drop stale keep-alive connections');
_client.close();
_client = Client();
}

static int _kdfMainIsolate(_KdfMainParams params) {
final dylib = _library;
assert(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ class KdfOperationsNativeLibrary implements IKdfOperations {
throw UnsupportedError('Native operations not available on this platform');
}

@override
void resetHttpClient() {
// No-op for stub
}

@override
void dispose() {
// No-op for stub
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ class KdfOperationsRemote implements IKdfOperations {
}
}

@override
void resetHttpClient() {
// No-op for remote operations - HTTP client is managed by http package
_log('resetHttpClient called on remote operations (no-op)');
}

@override
void dispose() {
// No-op for remote operations - HTTP client is managed externally
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,12 @@ class KdfOperationsWasm implements IKdfOperations {
}
}

@override
void resetHttpClient() {
// No-op for WASM operations - HTTP client is managed by browser
_log('resetHttpClient called on WASM operations (no-op)');
}

@override
void dispose() {
// Clean up any resources used by the WASM operations
Expand Down
Loading
Loading