11import 'package:bb_mobile/core/entities/signer_device_entity.dart' ;
2- import 'package:bb_mobile/core/entities/signer_entity.dart' ;
32import 'package:bb_mobile/core/ledger/data/datasources/ledger_device_datasource.dart' ;
43import 'package:bb_mobile/core/ledger/data/models/ledger_device_model.dart' ;
54import 'package:bb_mobile/core/ledger/domain/entities/ledger_device_entity.dart' ;
65import 'package:bb_mobile/core/ledger/domain/errors/ledger_exception.dart' ;
76import 'package:bb_mobile/core/ledger/domain/errors/ledger_failure.dart' ;
87import 'package:bb_mobile/core/ledger/domain/repositories/ledger_device_repository.dart' ;
9- import 'package:bb_mobile/core/settings/data/settings_repository.dart' ;
108import 'package:bb_mobile/core/utils/logger.dart' ;
119import 'package:bb_mobile/core/utils/result.dart' ;
1210import '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
1612class 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}
0 commit comments