Skip to content

Commit 2648915

Browse files
fix: restore device-busy error and harden failure mapping
1 parent c234f80 commit 2648915

38 files changed

Lines changed: 468 additions & 129 deletions

lib/core/ledger/data/repositories/ledger_device_repository_impl.dart

Lines changed: 42 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
11
import 'package:bb_mobile/core/entities/signer_device_entity.dart';
2-
import 'package:bb_mobile/core/entities/signer_entity.dart';
32
import 'package:bb_mobile/core/ledger/data/datasources/ledger_device_datasource.dart';
43
import 'package:bb_mobile/core/ledger/data/models/ledger_device_model.dart';
54
import 'package:bb_mobile/core/ledger/domain/entities/ledger_device_entity.dart';
65
import 'package:bb_mobile/core/ledger/domain/errors/ledger_exception.dart';
76
import 'package:bb_mobile/core/ledger/domain/errors/ledger_failure.dart';
87
import 'package:bb_mobile/core/ledger/domain/repositories/ledger_device_repository.dart';
9-
import 'package:bb_mobile/core/settings/data/settings_repository.dart';
108
import 'package:bb_mobile/core/utils/logger.dart';
119
import 'package:bb_mobile/core/utils/result.dart';
1210
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
13-
import 'package:bb_mobile/features/import_watch_only_wallet/watch_only_wallet_entity.dart';
14-
import 'package:satoshifier/satoshifier.dart' hide Network;
1511

1612
class LedgerDeviceRepositoryImpl implements LedgerDeviceRepository {
1713
final LedgerDeviceDatasource _datasource;
18-
final SettingsRepository _settingsRepository;
1914

20-
LedgerDeviceRepositoryImpl({
21-
required this._datasource,
22-
required this._settingsRepository,
23-
});
15+
LedgerDeviceRepositoryImpl({required this._datasource});
2416

2517
@override
2618
Future<Result<List<LedgerDeviceEntity>, LedgerFailure>> scanDevices({
@@ -33,68 +25,30 @@ class LedgerDeviceRepositoryImpl implements LedgerDeviceRepository {
3325
}
3426

3527
@override
36-
Future<Result<Null, LedgerFailure>> connectDevice(LedgerDeviceEntity device) {
28+
Future<Result<void, LedgerFailure>> connectDevice(LedgerDeviceEntity device) {
3729
return _guard(() async {
3830
await _datasource.connectDevice(device.toModel());
39-
return null;
4031
});
4132
}
4233

4334
@override
44-
Future<Result<WatchOnlyWalletEntity, LedgerFailure>> getWatchOnlyWallet(
45-
LedgerDeviceEntity device, {
46-
required String label,
47-
ScriptType scriptType = ScriptType.bip84,
48-
int account = 0,
49-
}) async {
50-
final Satoshifier watchOnly;
51-
switch (await _guard(() async {
52-
final settings = await _settingsRepository.fetch();
53-
final network = Network.fromEnvironment(
54-
isTestnet: settings.environment.isTestnet,
55-
isLiquid: false,
56-
);
57-
58-
final derivationPath =
59-
"m/${scriptType.purpose}'/${network.coinType}'/$account'";
35+
Future<Result<String, LedgerFailure>> getMasterFingerprint(
36+
LedgerDeviceEntity device,
37+
) {
38+
return _guard(() => _datasource.getMasterFingerprint(device.toModel()));
39+
}
6040

61-
final model = device.toModel();
62-
final masterFingerprint = await _datasource.getMasterFingerprint(model);
63-
final xpub = await _datasource.getXpub(
64-
model,
41+
@override
42+
Future<Result<String, LedgerFailure>> getXpub(
43+
LedgerDeviceEntity device, {
44+
required String derivationPath,
45+
required ScriptType scriptType,
46+
}) {
47+
return _guard(
48+
() => _datasource.getXpub(
49+
device.toModel(),
6550
derivationPath: derivationPath,
6651
scriptType: scriptType,
67-
);
68-
69-
final descriptor = Descriptor.fromStrings(
70-
fingerprint: masterFingerprint,
71-
path: derivationPath,
72-
xpub: xpub,
73-
);
74-
75-
return Satoshifier.watchOnlyDescriptor(descriptor: descriptor);
76-
})) {
77-
case Ok(:final value):
78-
watchOnly = value;
79-
case Err(:final failure):
80-
return Err(failure);
81-
}
82-
83-
if (watchOnly is! WatchOnlyDescriptor) {
84-
log.severe(
85-
message: 'Unexpected Ledger descriptor type',
86-
error: 'got ${watchOnly.runtimeType}',
87-
trace: StackTrace.current,
88-
);
89-
return const Err(LedgerUnexpectedFailure('unexpected descriptor type'));
90-
}
91-
92-
return Ok(
93-
WatchOnlyWalletEntity.descriptor(
94-
watchOnlyDescriptor: watchOnly,
95-
signer: SignerEntity.remote,
96-
label: label,
97-
signerDevice: device.deviceType,
9852
),
9953
);
10054
}
@@ -168,7 +122,7 @@ class LedgerDeviceRepositoryImpl implements LedgerDeviceRepository {
168122
} on DeviceNotFoundLedgerException {
169123
return const Err(LedgerDeviceNotFoundFailure());
170124
} on NoActiveConnectionLedgerException {
171-
return const Err(LedgerNoActiveConnectionFailure());
125+
return const Err(LedgerNoConnectionFailure());
172126
} on DeviceMismatchLedgerException {
173127
return const Err(LedgerDeviceMismatchFailure());
174128
} on InvalidMagicBytesLedgerException {
@@ -194,33 +148,39 @@ class LedgerDeviceRepositoryImpl implements LedgerDeviceRepository {
194148
/// only; it is never rendered by the UI.
195149
LedgerFailure _interpretRawError(Object error) {
196150
final raw = error.toString();
151+
152+
// The Ledger SDK reports a busy device as free text, not an APDU code.
153+
if (_deviceBusyPattern.hasMatch(raw)) return LedgerDeviceBusyFailure(raw);
154+
197155
final code = _extractApduCode(raw);
198156
if (code != null) {
199-
if (code.contains('6985')) return LedgerRejectedByUserFailure(raw);
200-
if (code.contains('5515')) return LedgerDeviceLockedFailure(raw);
201-
const appNotOpenCodes = ['6e01', '6a87', '6d02', '6511', '6e00'];
202-
if (appNotOpenCodes.any(code.contains)) {
157+
if (code == '6985') return LedgerRejectedByUserFailure(raw);
158+
if (code == '5515') return LedgerDeviceLockedFailure(raw);
159+
const appNotOpenCodes = {'6e01', '6a87', '6d02', '6511', '6e00'};
160+
if (appNotOpenCodes.contains(code)) {
203161
return LedgerBitcoinAppNotOpenFailure(raw);
204162
}
205163
}
206164
return LedgerUnexpectedFailure(raw);
207165
}
208166

167+
static final RegExp _deviceBusyPattern = RegExp(
168+
r'no other program|another program|already (in use|open)|'
169+
r'communicating with the ledger',
170+
caseSensitive: false,
171+
);
172+
173+
/// Extracts a normalized (lowercase, no `0x`) 4-hex-digit APDU status word.
174+
/// Matches are anchored on word boundaries and accept an optional `0x`
175+
/// prefix, so a status word survives but an incidental 4-hex run inside a
176+
/// txid/address does not get mistaken for one.
209177
String? _extractApduCode(String error) {
210-
final patterns = [
211-
RegExp(r'(?:0x\S*?|[0-9a-f]{4})(?= )'),
212-
RegExp('Exception:\\s*([0-9a-f]{4})'),
213-
RegExp('[0-9a-f]{4}'),
214-
];
215-
for (final pattern in patterns) {
216-
final match = pattern.firstMatch(error);
217-
if (match != null) {
218-
return match
219-
.group(0)
220-
?.replaceAll('0x', '')
221-
.replaceAll('Exception: ', '');
222-
}
223-
}
224-
return null;
178+
final match = _apduCodePattern.firstMatch(error);
179+
return match?.group(1)?.toLowerCase();
225180
}
181+
182+
static final RegExp _apduCodePattern = RegExp(
183+
r'\b(?:0x)?([0-9a-f]{4})\b',
184+
caseSensitive: false,
185+
);
226186
}

lib/core/ledger/domain/errors/ledger_failure.dart

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@ final class LedgerDeviceNotFoundFailure extends LedgerFailure {
2828
const LedgerDeviceNotFoundFailure([super.logMessage]);
2929
}
3030

31-
final class LedgerNoActiveConnectionFailure extends LedgerFailure {
32-
const LedgerNoActiveConnectionFailure([super.logMessage]);
33-
}
34-
3531
final class LedgerDeviceMismatchFailure extends LedgerFailure {
3632
const LedgerDeviceMismatchFailure([super.logMessage]);
3733
}
@@ -55,11 +51,19 @@ final class LedgerBitcoinAppNotOpenFailure extends LedgerFailure {
5551
const LedgerBitcoinAppNotOpenFailure([super.logMessage]);
5652
}
5753

58-
/// No connected device when an operation was requested.
54+
/// No active connection to a device. Raised both by the datasource (when an
55+
/// operation needs a live connection it doesn't have) and by the UI pre-check
56+
/// before an operation is requested; both surface the same message.
5957
final class LedgerNoConnectionFailure extends LedgerFailure {
6058
const LedgerNoConnectionFailure([super.logMessage]);
6159
}
6260

61+
/// Another program (e.g. Ledger Live) is communicating with the device, so the
62+
/// app cannot claim the connection. The user must close the other program.
63+
final class LedgerDeviceBusyFailure extends LedgerFailure {
64+
const LedgerDeviceBusyFailure([super.logMessage]);
65+
}
66+
6367
/// A required PSBT parameter was missing when signing.
6468
final class LedgerMissingPsbtFailure extends LedgerFailure {
6569
const LedgerMissingPsbtFailure([super.logMessage]);

lib/core/ledger/domain/repositories/ledger_device_repository.dart

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import 'package:bb_mobile/core/ledger/domain/entities/ledger_device_entity.dart'
33
import 'package:bb_mobile/core/ledger/domain/errors/ledger_failure.dart';
44
import 'package:bb_mobile/core/utils/result.dart';
55
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
6-
import 'package:bb_mobile/features/import_watch_only_wallet/watch_only_wallet_entity.dart';
76
import 'package:meta/meta.dart';
87

98
abstract class LedgerDeviceRepository {
@@ -13,14 +12,18 @@ abstract class LedgerDeviceRepository {
1312
});
1413

1514
@useResult
16-
Future<Result<Null, LedgerFailure>> connectDevice(LedgerDeviceEntity device);
15+
Future<Result<void, LedgerFailure>> connectDevice(LedgerDeviceEntity device);
1716

1817
@useResult
19-
Future<Result<WatchOnlyWalletEntity, LedgerFailure>> getWatchOnlyWallet(
18+
Future<Result<String, LedgerFailure>> getMasterFingerprint(
19+
LedgerDeviceEntity device,
20+
);
21+
22+
@useResult
23+
Future<Result<String, LedgerFailure>> getXpub(
2024
LedgerDeviceEntity device, {
21-
required String label,
22-
ScriptType scriptType = ScriptType.bip84,
23-
int account = 0,
25+
required String derivationPath,
26+
required ScriptType scriptType,
2427
});
2528

2629
@useResult

lib/core/ledger/domain/usecases/connect_ledger_device_usecase.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class ConnectLedgerDeviceUsecase {
1010
ConnectLedgerDeviceUsecase({required this._repository});
1111

1212
@useResult
13-
Future<Result<Null, LedgerFailure>> execute(LedgerDeviceEntity device) {
13+
Future<Result<void, LedgerFailure>> execute(LedgerDeviceEntity device) {
1414
return _repository.connectDevice(device);
1515
}
1616
}
Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,85 @@
1+
import 'package:bb_mobile/core/entities/signer_entity.dart';
12
import 'package:bb_mobile/core/ledger/domain/entities/ledger_device_entity.dart';
23
import 'package:bb_mobile/core/ledger/domain/errors/ledger_failure.dart';
34
import 'package:bb_mobile/core/ledger/domain/repositories/ledger_device_repository.dart';
5+
import 'package:bb_mobile/core/settings/data/settings_repository.dart';
46
import 'package:bb_mobile/core/utils/result.dart';
57
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
68
import 'package:bb_mobile/features/import_watch_only_wallet/watch_only_wallet_entity.dart';
79
import 'package:meta/meta.dart';
10+
import 'package:satoshifier/satoshifier.dart' hide Network;
811

912
class GetLedgerWatchOnlyWalletUsecase {
1013
final LedgerDeviceRepository _repository;
14+
final SettingsRepository _settingsRepository;
1115

12-
GetLedgerWatchOnlyWalletUsecase({required this._repository});
16+
GetLedgerWatchOnlyWalletUsecase({
17+
required this._repository,
18+
required this._settingsRepository,
19+
});
1320

1421
@useResult
1522
Future<Result<WatchOnlyWalletEntity, LedgerFailure>> execute({
1623
required String label,
1724
required LedgerDeviceEntity device,
1825
ScriptType scriptType = ScriptType.bip84,
1926
int account = 0,
20-
}) {
21-
return _repository.getWatchOnlyWallet(
27+
}) async {
28+
final String derivationPath;
29+
try {
30+
final settings = await _settingsRepository.fetch();
31+
final network = Network.fromEnvironment(
32+
isTestnet: settings.environment.isTestnet,
33+
isLiquid: false,
34+
);
35+
derivationPath =
36+
"m/${scriptType.purpose}'/${network.coinType}'/$account'";
37+
} catch (e) {
38+
return Err(LedgerUnexpectedFailure('failed to resolve settings: $e'));
39+
}
40+
41+
final String masterFingerprint;
42+
switch (await _repository.getMasterFingerprint(device)) {
43+
case Ok(:final value):
44+
masterFingerprint = value;
45+
case Err(:final failure):
46+
return Err(failure);
47+
}
48+
49+
final String xpub;
50+
switch (await _repository.getXpub(
2251
device,
23-
label: label,
52+
derivationPath: derivationPath,
2453
scriptType: scriptType,
25-
account: account,
26-
);
54+
)) {
55+
case Ok(:final value):
56+
xpub = value;
57+
case Err(:final failure):
58+
return Err(failure);
59+
}
60+
61+
try {
62+
final descriptor = Descriptor.fromStrings(
63+
fingerprint: masterFingerprint,
64+
path: derivationPath,
65+
xpub: xpub,
66+
);
67+
final watchOnly = Satoshifier.watchOnlyDescriptor(descriptor: descriptor);
68+
69+
if (watchOnly is! WatchOnlyDescriptor) {
70+
return const Err(LedgerUnexpectedFailure('unexpected descriptor type'));
71+
}
72+
73+
return Ok(
74+
WatchOnlyWalletEntity.descriptor(
75+
watchOnlyDescriptor: watchOnly,
76+
signer: SignerEntity.remote,
77+
label: label,
78+
signerDevice: device.deviceType,
79+
),
80+
);
81+
} catch (e) {
82+
return Err(LedgerUnexpectedFailure('failed to build descriptor: $e'));
83+
}
2784
}
2885
}

lib/core/ledger/ledger_locator.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ class LedgerLocator {
2020
locator.registerLazySingleton<LedgerDeviceRepository>(
2121
() => LedgerDeviceRepositoryImpl(
2222
datasource: locator<LedgerDeviceDatasource>(),
23-
settingsRepository: locator<SettingsRepository>(),
2423
),
2524
);
2625
}
@@ -39,6 +38,7 @@ class LedgerLocator {
3938
locator.registerFactory<GetLedgerWatchOnlyWalletUsecase>(
4039
() => GetLedgerWatchOnlyWalletUsecase(
4140
repository: locator<LedgerDeviceRepository>(),
41+
settingsRepository: locator<SettingsRepository>(),
4242
),
4343
);
4444
locator.registerFactory<SignPsbtLedgerUsecase>(

lib/features/ledger/presentation/cubit/ledger_operation_cubit.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ class LedgerOperationCubit extends Cubit<LedgerOperationState> {
5151
case Err(:final failure):
5252
return _emitFailure(failure);
5353
}
54+
if (devices.isEmpty) {
55+
return _emitFailure(const LedgerNoDevicesFoundFailure());
56+
}
5457

5558
emit(
5659
state.copyWith(

lib/features/ledger/presentation/ledger_failure_l10n.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ extension LedgerFailureL10n on LedgerFailure {
1111
LedgerMultipleDevicesFoundFailure() =>
1212
context.loc.ledgerErrorMultipleDevicesFound,
1313
LedgerDeviceNotFoundFailure() => context.loc.ledgerErrorDeviceNotFound,
14-
LedgerNoActiveConnectionFailure() => context.loc.ledgerErrorNoConnection,
1514
LedgerNoConnectionFailure() => context.loc.ledgerErrorNoConnection,
15+
LedgerDeviceBusyFailure() => context.loc.ledgerErrorDeviceBusy,
1616
LedgerDeviceMismatchFailure() => context.loc.ledgerErrorDeviceMismatch,
1717
LedgerInvalidPsbtFailure() => context.loc.ledgerErrorInvalidPsbt,
1818
LedgerRejectedByUserFailure() => context.loc.ledgerErrorRejectedByUser,

localization/app_ar.arb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8728,6 +8728,10 @@
87288728
"@backupSettingsViewVaultKey": {
87298729
"description": "Button text to view vault decryption key"
87308730
},
8731+
"ledgerErrorDeviceBusy": "برنامج آخر يستخدم جهاز Ledger الخاص بك. الرجاء إغلاقه (على سبيل المثال، Ledger Live) والمحاولة مرة أخرى.",
8732+
"@ledgerErrorDeviceBusy": {
8733+
"description": "Error message when another program is communicating with the Ledger device"
8734+
},
87318735
"ledgerErrorNoConnection": "No Ledger connection available",
87328736
"@ledgerErrorNoConnection": {
87338737
"description": "Error message when no Ledger connection is available"

0 commit comments

Comments
 (0)