Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9fccee3
feat(auth): add polling stream for Trezor connection (#125)
takenagain Jul 7, 2025
7ebdf4c
feat(trezor,auth): sign user out if device is disconnected
takenagain Jul 7, 2025
32c0006
fix(trezor): use snake case for keys and pascal case for values
takenagain Jul 7, 2025
f1d1493
fix(trezor): improve connection status stream error handling
takenagain Jul 7, 2025
eae55fa
refactor(trezor): improve Trezor connection monitoring with timeout a…
takenagain Aug 4, 2025
69f3c88
Merge branch 'dev' into feat/trezor-connection-status
takenagain Aug 8, 2025
25265fd
refactor(review): address PR nitpicks
takenagain Aug 8, 2025
1044a0b
refactor(review): add convenience methods and constructors
takenagain Aug 8, 2025
cb186ed
test: add unit tests for the new poll utility
takenagain Aug 11, 2025
4b1f5fd
refactor(trezor): add logging statements to auth mixin
takenagain Aug 11, 2025
9f2dc1c
test(trezor): add unit test cases for newly added interfaces
takenagain Aug 12, 2025
09f622e
Merge remote-tracking branch 'origin/dev' into feat/trezor-connection…
takenagain Aug 12, 2025
a8ca5e0
refactor(trezor): await close and stop functions and yield error
takenagain Aug 12, 2025
4ec1b11
test(trezor): expand connection error handling test
takenagain Aug 12, 2025
71e1381
fix(trezor): mitigate Trezor pin/passphrase exposure with converter
takenagain Aug 13, 2025
323dd31
fix(trezor): assume initial state as available and extract local vari…
takenagain Aug 13, 2025
8d1edc0
Merge branch 'dev' of https://github.com/KomodoPlatform/komodo-defi-s…
CharlVS Aug 13, 2025
2f91435
chore: rename dragon charts folder
CharlVS Aug 13, 2025
8dec689
fix: fix broken package reference
CharlVS Aug 13, 2025
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
1 change: 1 addition & 0 deletions packages/komodo_defi_local_auth/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ migrate_working_dir/
/build/
pubspec.lock
build/
web/

# Web related
lib/generated_plugin_registrant.dart
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
library _trezor;

export 'trezor_auth_service.dart';
export 'trezor_connection_monitor.dart';
export 'trezor_connection_status.dart';
export 'trezor_exception.dart';
export 'trezor_initialization_state.dart';
export 'trezor_repository.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,27 @@ import 'package:logging/logging.dart';
/// handling passphrase requirements and ignoring PIN prompts. All other
/// [IAuthService] methods are delegated to the composed auth service.
class TrezorAuthService implements IAuthService {
TrezorAuthService(this._authService, this._trezor);
TrezorAuthService(
this._authService,
this._trezor, {
TrezorConnectionMonitor? connectionMonitor,
FlutterSecureStorage? secureStorage,
String Function(int length)? passwordGenerator,
}) : _connectionMonitor =
connectionMonitor ?? TrezorConnectionMonitor(_trezor),
_secureStorage = secureStorage ?? const FlutterSecureStorage(),
_generatePassword =
passwordGenerator ?? SecurityUtils.generatePasswordSecure;

static const String trezorWalletName = 'My Trezor';
static const String _passwordKey = 'trezor_wallet_password';
static final _log = Logger('TrezorAuthService');

final IAuthService _authService;
final TrezorRepository _trezor;
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
final FlutterSecureStorage _secureStorage;
final TrezorConnectionMonitor _connectionMonitor;
final String Function(int length) _generatePassword;

Future<void> provideTrezorPin(int taskId, String pin) =>
_trezor.providePin(taskId, pin);
Expand Down Expand Up @@ -100,10 +112,16 @@ class TrezorAuthService implements IAuthService {
Stream<KdfUser?> get authStateChanges => _authService.authStateChanges;

@override
Future<void> dispose() => _authService.dispose();
Future<void> dispose() async {
_connectionMonitor.dispose();
await _authService.dispose();
}

@override
Future<void> signOut() => _authService.signOut();
Future<void> signOut() async {
await _stopConnectionMonitoring();
await _authService.signOut();
}

@override
Future<void> deleteWallet({
Expand All @@ -126,7 +144,11 @@ class TrezorAuthService implements IAuthService {
}

try {
return await _initializeTrezorWithPassphrase(passphrase: password);
final user = await _initializeTrezorWithPassphrase(passphrase: password);

_startConnectionMonitoring();

Comment thread
takenagain marked this conversation as resolved.
return user;
} catch (e) {
await _signOutCurrentTrezorUser();

Expand Down Expand Up @@ -154,7 +176,11 @@ class TrezorAuthService implements IAuthService {
}

try {
return await _initializeTrezorWithPassphrase(passphrase: password);
final user = await _initializeTrezorWithPassphrase(passphrase: password);

_startConnectionMonitoring();

return user;
} catch (e) {
await _signOutCurrentTrezorUser();

Expand All @@ -180,7 +206,7 @@ class TrezorAuthService implements IAuthService {

if (existing != null) return existing;

final newPassword = SecurityUtils.generatePasswordSecure(16);
final newPassword = _generatePassword(16);
await _secureStorage.write(key: _passwordKey, value: newPassword);
return newPassword;
}
Expand All @@ -189,11 +215,34 @@ class TrezorAuthService implements IAuthService {
Future<void> clearTrezorPassword() =>
_secureStorage.delete(key: _passwordKey);

/// Start monitoring Trezor connection status after successful authentication.
/// This will automatically sign out if the device becomes disconnected.
void _startConnectionMonitoring({String? devicePubkey}) {
_connectionMonitor.startMonitoring(
devicePubkey: devicePubkey,
onConnectionLost: () async {
_log.warning('Trezor connection lost, signing out user');
await _signOutCurrentTrezorUser();
},
onStatusChanged: (status) {
_log.fine('Trezor connection status: ${status.value}');
},
);
}
Comment thread
takenagain marked this conversation as resolved.

/// Stop monitoring Trezor connection status.
Future<void> _stopConnectionMonitoring() async {
if (_connectionMonitor.isMonitoring) {
await _connectionMonitor.stopMonitoring();
}
}

/// Signs out the current user if they are using the Trezor wallet
Future<void> _signOutCurrentTrezorUser() async {
final current = await _authService.getActiveUser();
if (current?.walletId.name == trezorWalletName) {
_log.warning("Signing out current '${current?.walletId.name}' user");
await _stopConnectionMonitoring();
try {
await _authService.signOut();
} catch (_) {
Expand Down Expand Up @@ -286,6 +335,7 @@ class TrezorAuthService implements IAuthService {
if (trezorState.status == AuthenticationStatus.completed) {
final user = await _authService.getActiveUser();
if (user != null) {
_startConnectionMonitoring();
yield AuthenticationState.completed(user);
} else {
yield AuthenticationState.error(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:komodo_defi_local_auth/src/trezor/trezor_connection_status.dart';
import 'package:komodo_defi_local_auth/src/trezor/trezor_repository.dart';
import 'package:logging/logging.dart';

/// Service responsible for monitoring Trezor device connection status
/// and providing callbacks for connection state changes.
class TrezorConnectionMonitor {
TrezorConnectionMonitor(this._trezorRepository);

static final _log = Logger('TrezorConnectionMonitor');

final TrezorRepository _trezorRepository;
StreamSubscription<TrezorConnectionStatus>? _connectionSubscription;
TrezorConnectionStatus? _lastStatus;

/// Start monitoring the Trezor connection status.
///
/// [onConnectionLost] will be called when the device becomes disconnected
/// or unreachable.
/// [onConnectionRestored] will be called when the device becomes connected
/// after being disconnected/unreachable.
/// [onStatusChanged] will be called for any status change.
/// [maxDuration] sets the maximum time to monitor before timing out. If null,
/// monitoring continues indefinitely until stopped or disconnected.
void startMonitoring({
String? devicePubkey,
Duration pollInterval = const Duration(seconds: 1),
Duration? maxDuration,
VoidCallback? onConnectionLost,
VoidCallback? onConnectionRestored,
void Function(TrezorConnectionStatus)? onStatusChanged,
}) {
_log.info('Starting Trezor connection monitoring');

// Stop any existing monitoring safely before starting a new one.
final previousSubscription = _connectionSubscription;
if (previousSubscription != null) {
_log.info('Stopping previous Trezor connection monitoring');
_connectionSubscription = null;
_lastStatus = null;
unawaited(previousSubscription.cancel());
}

_connectionSubscription = _trezorRepository
.watchConnectionStatus(
devicePubkey: devicePubkey,
pollInterval: pollInterval,
maxDuration: maxDuration,
)
.listen(
(status) {
_log.fine('Connection status changed: ${status.value}');

final previousStatus = _lastStatus;
_lastStatus = status;

onStatusChanged?.call(status);

if (status.isUnavailable &&
(previousStatus?.isAvailable ?? false)) {
_log.warning('Trezor connection lost: ${status.value}');
onConnectionLost?.call();
}

if (status.isAvailable &&
(previousStatus?.isUnavailable ?? false)) {
_log.info('Trezor connection restored');
onConnectionRestored?.call();
}
Comment thread
takenagain marked this conversation as resolved.
Comment thread
takenagain marked this conversation as resolved.
Comment thread
takenagain marked this conversation as resolved.
},
onError: (Object error, StackTrace stackTrace) {
_log.severe(
'Error monitoring Trezor connection: $error',
error,
stackTrace,
);
// Only call onConnectionLost if this is a real connection error, not a disposal
if (_connectionSubscription != null) {
onConnectionLost?.call();
}
},
onDone: () {
_log.info('Trezor connection monitoring stopped');
// Underlying stream ended; mark as not monitoring while keeping
// the last known status for inspection.
_connectionSubscription = null;
},
);
}

/// Stop monitoring the Trezor connection status.
Future<void> stopMonitoring() async {
if (_connectionSubscription != null) {
_log.info('Stopping Trezor connection monitoring');
await _connectionSubscription?.cancel();
_connectionSubscription = null;
_lastStatus = null;
}
}

/// Get the last known connection status.
TrezorConnectionStatus? get lastKnownStatus => _lastStatus;

/// Check if monitoring is currently active.
bool get isMonitoring => _connectionSubscription != null;

/// Dispose of the monitor and clean up resources.
void dispose() {
// Make monitoring appear stopped synchronously.
final previousSubscription = _connectionSubscription;
_connectionSubscription = null;
_lastStatus = null;
if (previousSubscription != null) {
unawaited(previousSubscription.cancel());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/// Enum representing Trezor device connection status
enum TrezorConnectionStatus {
/// Device is connected and ready for operations
connected,

/// Device is disconnected
disconnected,

/// Device is busy with another operation
busy,

/// Device is unreachable (possibly hardware issue or driver problem)
unreachable,

/// Unknown status (for unrecognized status strings)
unknown;

/// Parse a string status from the API response into enum
static TrezorConnectionStatus fromString(String status) {
switch (status.toLowerCase()) {
case 'connected':
return TrezorConnectionStatus.connected;
case 'disconnected':
return TrezorConnectionStatus.disconnected;
case 'busy':
return TrezorConnectionStatus.busy;
case 'unreachable':
return TrezorConnectionStatus.unreachable;
default:
return TrezorConnectionStatus.unknown;
}
}

/// Human-readable label for display purposes
String get value {
switch (this) {
case TrezorConnectionStatus.connected:
return 'Connected';
case TrezorConnectionStatus.disconnected:
return 'Disconnected';
case TrezorConnectionStatus.busy:
return 'Busy';
case TrezorConnectionStatus.unreachable:
return 'Unreachable';
case TrezorConnectionStatus.unknown:
return 'Unknown';
}
}

/// Lowercase identifier used by the API
String get apiValue => name; // matches fromString expectations

/// Check if the status indicates the device is available for operations
bool get isAvailable => this == TrezorConnectionStatus.connected;

/// Check if the status indicates the device is not available
bool get isUnavailable =>
this == TrezorConnectionStatus.disconnected ||
this == TrezorConnectionStatus.unreachable ||
this == TrezorConnectionStatus.busy;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Busy Trezor Misinterpreted as Unavailable

The TrezorConnectionStatus.isUnavailable getter incorrectly includes TrezorConnectionStatus.busy. This causes the TrezorConnectionMonitor to treat a busy Trezor device as unavailable, triggering onConnectionLost and spuriously logging out users. This occurs during normal operations when the device is temporarily busy, contrary to the intended behavior of signing out only on actual connection loss (disconnected or unreachable).

Fix in Cursor Fix in Web

/// Check if the device should continue being monitored
bool get shouldContinueMonitoring =>
this != TrezorConnectionStatus.disconnected;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:komodo_defi_local_auth/komodo_defi_local_auth.dart';
import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart';
import 'package:komodo_defi_types/komodo_defi_types.dart';

part 'trezor_initialization_state.freezed.dart';

Expand Down
Loading
Loading