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 @@ -65,7 +65,7 @@
"coins": {
"fetch_at_build_enabled": true,
"update_commit_on_build": true,
"bundled_coins_repo_commit": "1d2a5c9c4d23416df2fa1c5e2f263a244a09704d",
"bundled_coins_repo_commit": "46587568ac5ed542544dc9bd68bab35d7d818cf2",
"coins_repo_api_url": "https://api.github.com/repos/GLEECBTC/coins",
"coins_repo_content_url": "https://raw.githubusercontent.com/GLEECBTC/coins",
"coins_repo_branch": "master",
Expand Down
101 changes: 72 additions & 29 deletions packages/komodo_defi_framework/lib/komodo_defi_framework.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export 'package:komodo_defi_framework/src/streaming/events/kdf_event.dart';
export 'src/operations/kdf_operations_interface.dart';

class KomodoDefiFramework implements ApiClient {
static const Duration _versionProbeTimeout = Duration(seconds: 2);
static const Duration _stopPollInterval = Duration(milliseconds: 250);
static const Duration _stopSettleDelay = Duration(milliseconds: 250);

factory KomodoDefiFramework.create({
required IKdfHostConfig hostConfig,
void Function(String)? externalLogger,
Expand Down Expand Up @@ -162,24 +166,44 @@ class KomodoDefiFramework implements ApiClient {
_log('Stopping KDF...');
final result = await _kdfOperations.kdfStop();
_log('KDF stop result: $result');
// Await a max of 5 seconds for KDF to stop. Check every 500ms.
for (var i = 0; i < 10; i++) {
await Future<void>.delayed(const Duration(milliseconds: 500));
if (!await isRunning()) {
break;
}
if (i == 9) {
throw Exception('Error stopping KDF: KDF did not stop in time.');

// Drop any stale keep-alive socket before verifying shutdown. Otherwise,
// the post-stop version() fallback can hang on Android while the native
// thread is already tearing down.
resetHttpClient();

// Wait for native status to settle without probing RPC over HTTP.
for (var i = 0; i < 20; i++) {
await Future<void>.delayed(_stopPollInterval);
final stillRunning = await isRunning(allowVersionFallback: false);
if (!stillRunning) {
await Future<void>.delayed(_stopSettleDelay);
if (!await isRunning(allowVersionFallback: false)) {
return result;
}
}
}

return result;
throw Exception('Error stopping KDF: KDF did not stop in time.');
}

Future<bool> isRunning() async {
Future<bool> isRunning({bool allowVersionFallback = true}) async {
final nativeRunning = await _kdfOperations.isRunning();
if (nativeRunning) {
return true;
}

if (!allowVersionFallback) {
_log('KDF is not running.');
return false;
}

final running =
await _kdfOperations.isRunning() ||
await _kdfOperations.version() != null;
await _kdfOperations.version().timeout(
_versionProbeTimeout,
onTimeout: () => null,
) !=
null;
if (!running) {
_log('KDF is not running.');
}
Expand All @@ -188,15 +212,23 @@ class KomodoDefiFramework implements ApiClient {

Future<String?> version() async {
final stopwatch = Stopwatch()..start();
_log('version(): Starting version RPC call via ${_kdfOperations.operationsName}');
_log(
'version(): Starting version RPC call via ${_kdfOperations.operationsName}',
);
try {
final version = await _kdfOperations.version();
final version = await _kdfOperations.version().timeout(
_versionProbeTimeout,
);
stopwatch.stop();
_log('version(): Completed in ${stopwatch.elapsedMilliseconds}ms, result=$version');
_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');
_log(
'version(): Failed after ${stopwatch.elapsedMilliseconds}ms with error: $e',
);
rethrow;
}
}
Expand All @@ -205,7 +237,7 @@ class KomodoDefiFramework implements ApiClient {
/// 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).
Expand All @@ -217,7 +249,7 @@ class KomodoDefiFramework implements ApiClient {
_log('KDF health check failed: version call returned null');
return false;
}

_log('KDF health check passed: version=$versionCheck');
return true;
} catch (e) {
Expand Down Expand Up @@ -279,26 +311,37 @@ class KomodoDefiFramework implements ApiClient {
return response;
} catch (e) {
stopwatch.stop();

// 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')
);
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)';
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)';
_logger.severe(
'[RPC] ${method ?? 'unknown'} failed: KDF transport error $errorType. '
'Resetting HTTP client to drop stale connections.',
Expand Down
122 changes: 87 additions & 35 deletions packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ class KdfAuthService implements IAuthService {
List<KdfUser>? _usersCache;
DateTime? _usersCacheTimestamp;
final Duration _usersCacheTtl = const Duration(minutes: 5);
static const Duration _kdfRpcReadyTimeout = Duration(seconds: 15);
static const Duration _kdfRpcProbeTimeout = Duration(seconds: 2);
static const Duration _kdfRpcPollInterval = Duration(milliseconds: 250);
static const Duration _startupSensitiveRpcTimeout = Duration(seconds: 10);

ApiClient get _client => _kdfFramework.client;
late final methods = KomodoDefiRpcMethods(_client);
Expand Down Expand Up @@ -254,42 +258,85 @@ class KdfAuthService implements IAuthService {
),
Mnemonic? mnemonic,
}) async {
await _ensureKdfRunning();
_logger.info(
'[$_sessionId] register: Starting registration for wallet: $walletName',
);
final registerStopwatch = Stopwatch()..start();

await _runReadOperation(() async {
final walletExists = await _walletExists(walletName);
if (walletExists) {
throw AuthException(
'Wallet already exists',
type: AuthExceptionType.generalAuthError,
try {
final ensureStartStopwatch = Stopwatch()..start();
await _ensureKdfRunning();
ensureStartStopwatch.stop();
_logger.info(
'[$_sessionId] register: ensure no-auth start completed in '
'${ensureStartStopwatch.elapsedMilliseconds}ms',
);

final walletExistsStopwatch = Stopwatch()..start();
await _runReadOperation(() async {
final walletExists = await _walletExists(walletName);
if (walletExists) {
throw AuthException(
'Wallet already exists',
type: AuthExceptionType.generalAuthError,
);
}
});
walletExistsStopwatch.stop();
_logger.info(
'[$_sessionId] register: wallet existence read completed in '
'${walletExistsStopwatch.elapsedMilliseconds}ms',
);

// replaces the __assertWalletOrStop method - wait for read/write locks to
// be released here.
// can be used outside of a lock, since both functions are public-facing
// and manage their own read/write locks
final stopStopwatch = Stopwatch()..start();
if (await isSignedIn()) {
await signOut();
stopStopwatch.stop();
_logger.info(
'[$_sessionId] register: stop phase completed in '
'${stopStopwatch.elapsedMilliseconds}ms',
);
} else {
stopStopwatch.stop();
_logger.info(
'[$_sessionId] register: no active session to stop '
'(${stopStopwatch.elapsedMilliseconds}ms)',
);
}
});

// replaces the __assertWalletOrStop method - wait for read/write locks to
// be released here.
// can be used outside of a lock, since both functions are public-facing
// and manage their own read/write locks
if (await isSignedIn()) {
await signOut();
}

final config = await _generateStartupConfig(
walletName: walletName,
walletPassword: password,
allowRegistrations: true,
plaintextMnemonic: mnemonic?.plaintextMnemonic,
hdEnabled: options.derivationMethod == DerivationMethod.hdWallet,
allowWeakPassword: options.allowWeakPassword,
);
final config = await _generateStartupConfig(
walletName: walletName,
walletPassword: password,
allowRegistrations: true,
plaintextMnemonic: mnemonic?.plaintextMnemonic,
hdEnabled: options.derivationMethod == DerivationMethod.hdWallet,
allowWeakPassword: options.allowWeakPassword,
);

return _lockWriteOperation(() async {
final isImported = mnemonic != null;
final currentUser = await _registerNewUser(config, options, isImported);
_emitAuthStateChange(currentUser);
_invalidateUsersCache();
return currentUser;
});
return _lockWriteOperation(() async {
final writePathStopwatch = Stopwatch()..start();
final isImported = mnemonic != null;
final currentUser = await _registerNewUser(config, options, isImported);
writePathStopwatch.stop();
_logger.info(
'[$_sessionId] register: registration write path completed in '
'${writePathStopwatch.elapsedMilliseconds}ms',
);
_emitAuthStateChange(currentUser);
_invalidateUsersCache();
return currentUser;
});
} finally {
registerStopwatch.stop();
_logger.info(
'[$_sessionId] register: Finished in '
'${registerStopwatch.elapsedMilliseconds}ms',
);
}
}

@override
Expand All @@ -304,7 +351,10 @@ class KdfAuthService implements IAuthService {
return _usersCache!;
}

final walletNames = await _client.rpc.wallet.getWalletNames();
final walletNames = await _runStartupSensitiveRpc(
phase: 'get_wallet_names',
operation: () => _client.rpc.wallet.getWalletNames(),
);

final users = await Future.wait(
walletNames.walletNames.map((name) async {
Expand Down Expand Up @@ -1006,12 +1056,14 @@ class KdfAuthService implements IAuthService {
throw KdfExtensions._mapStartupErrorToAuthException(result);
}

_logger.info('[$_sessionId] _forceStartKdf: Waiting for RPC to be up');
_kdfFramework.resetHttpClient();
_logger.info('[$_sessionId] _forceStartKdf: Waiting for RPC to be ready');
final waitStopwatch = Stopwatch()..start();
await _waitUntilKdfRpcIsUp();
await _waitUntilKdfRpcReady();
waitStopwatch.stop();
_logger.info(
'[$_sessionId] _forceStartKdf: RPC is up after ${waitStopwatch.elapsedMilliseconds}ms',
'[$_sessionId] _forceStartKdf: RPC ready after '
'${waitStopwatch.elapsedMilliseconds}ms',
);
});
}
Expand Down
Loading
Loading