Cw 1291 ledger fixes (#2741)

* fix: handle unexpected ViewModel in ledger connection process * fix: add missing exception handler for `_QueuedFuture.execute` in `exception_handler.dart` * fix: handle hardware wallet connection flow in exchange trade page * refactor: replace hardware wallet conditional logic with switch statement for improved readability and future extensibility * feat: add hardware wallet support for dEURO savings actions and approval flow --------- Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>

Omar Hatem committed Dec 20, 2025 at 01:58 UTC 6c12ed157d934fd224590bb1f4d6a6e527b5e19d
5 files changed +112 -38
cw_evm/lib/evm_chain_wallet.dart
+27 -17
@@ -210,23 +210,33 @@ abstract class EVMChainWalletBase
210 // check for Already existing scam tokens, cuz users can get scammed twice ¯\_(ツ)_/¯
211 await _checkForExistingScamTokens();
212
213 - if (walletInfo.hardwareWalletType == HardwareWalletType.ledger) {
214 - _evmChainPrivateKey = EvmLedgerCredentials(walletInfo.address);
215 - walletAddresses.address = walletInfo.address;
216 - } else if (walletInfo.hardwareWalletType == HardwareWalletType.bitbox) {
217 - _evmChainPrivateKey = EvmBitboxCredentials(walletInfo.address);
218 - walletAddresses.address = walletInfo.address;
219 - } else if (walletInfo.hardwareWalletType == HardwareWalletType.trezor) {
220 - _evmChainPrivateKey = EvmTrezorCredentials(walletInfo.address);
221 - walletAddresses.address = walletInfo.address;
222 - } else {
223 - _evmChainPrivateKey = await getPrivateKey(
224 - mnemonic: _mnemonic,
225 - privateKey: _hexPrivateKey,
226 - password: _password,
227 - passphrase: passphrase,
228 - );
229 - walletAddresses.address = _evmChainPrivateKey.address.hexEip55;
213 + switch(walletInfo.hardwareWalletType) {
214 + case HardwareWalletType.ledger:
215 + _evmChainPrivateKey = EvmLedgerCredentials(walletInfo.address);
216 + walletAddresses.address = walletInfo.address;
217 + break;
218 + case HardwareWalletType.bitbox:
219 + _evmChainPrivateKey = EvmBitboxCredentials(walletInfo.address);
220 + walletAddresses.address = walletInfo.address;
221 + break;
222 + case HardwareWalletType.trezor:
223 + _evmChainPrivateKey = EvmTrezorCredentials(walletInfo.address);
224 + walletAddresses.address = walletInfo.address;
225 + break;
226 + case HardwareWalletType.cupcake:
227 + case HardwareWalletType.coldcard:
228 + case HardwareWalletType.seedsigner:
229 + case HardwareWalletType.keystone:
230 + throw UnimplementedError();
231 + case null:
232 + _evmChainPrivateKey = await getPrivateKey(
233 + mnemonic: _mnemonic,
234 + privateKey: _hexPrivateKey,
235 + password: _password,
236 + passphrase: passphrase,
237 + );
238 + walletAddresses.address = _evmChainPrivateKey.address.hexEip55;
239 + break;
240 }
241 await save();
242 }
lib/di.dart
+10 -6
@@ -1612,13 +1612,17 @@ Future<void> setup({
1612 getIt.registerFactory(() => DevExchangeProviderLogsPage(getIt.get<ExchangeProviderLogsViewModel>()));
1613
1614 getIt.registerFactory(() => StartTorPage(StartTorViewModel(),));
1615 -
1615 +
1616 getIt.registerFactory(() => DEuroViewModel(
1617 - getIt<AppStore>(),
1618 - getIt<BalanceViewModel>(),
1619 - getIt<SettingsStore>(),
1620 - getIt<FiatConversionStore>(),
1621 - ));
1617 + getIt<AppStore>(),
1618 + getIt<BalanceViewModel>(),
1619 + getIt<SettingsStore>(),
1620 + getIt<FiatConversionStore>(),
1621 + getIt.get<AppStore>().wallet!.isHardwareWallet
1622 + ? getIt<HardwareWalletViewModel>(
1623 + param1: getIt.get<AppStore>().wallet!.hardwareWalletType!)
1624 + : null,
1625 + ));
1626
1627 getIt.registerFactory(() => DEuroSavingsPage(getIt<DEuroViewModel>()));
1628
lib/src/screens/exchange_trade/exchange_trade_page.dart
+26 -1
@@ -1,6 +1,7 @@
1 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
2 import 'package:cake_wallet/reactions/wallet_connect.dart';
3 import 'package:cake_wallet/routes.dart';
4 +import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
5 import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
6 import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart';
7 import 'package:cake_wallet/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart';
@@ -190,7 +191,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
191 isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty ||
192 sendingState is ExecutedSuccessfullyState,
193 isLoading: sendingState is IsExecutingState,
193 - onPressed: () => widget.exchangeTradeViewModel.confirmSending(),
194 + onPressed: _onPressedSendFromCakeWallet,
195 text: S.current.send_from_cake_wallet,
196 color: Theme.of(context).colorScheme.primary,
197 textColor: Theme.of(context).colorScheme.onPrimary,
@@ -204,6 +205,30 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
205 );
206 }
207
208 + Future<void> _onPressedSendFromCakeWallet() async {
209 + final sendVM = widget.exchangeTradeViewModel.sendViewModel;
210 +
211 + if (sendVM.wallet.isHardwareWallet) {
212 + if (!sendVM.hardwareWalletViewModel!.isConnected) {
213 + await Navigator.of(context).pushNamed(Routes.connectDevices,
214 + arguments: ConnectDevicePageParams(
215 + walletType: sendVM.walletType,
216 + hardwareWalletType:
217 + sendVM.wallet.walletInfo.hardwareWalletType!,
218 + onConnectDevice: (context, _) {
219 + print(sendVM.wallet);
220 + sendVM.hardwareWalletViewModel!.initWallet(sendVM.wallet);
221 + Navigator.of(context).pop();
222 + },
223 + ));
224 + } else {
225 + sendVM.hardwareWalletViewModel!.initWallet(sendVM.wallet);
226 + }
227 + }
228 +
229 + widget.exchangeTradeViewModel.confirmSending();
230 + }
231 +
232 BuildContext? dialogContext;
233 BuildContext? loadingBottomSheetContext;
234
lib/src/screens/integrations/deuro/savings_page.dart
+41 -8
@@ -1,6 +1,8 @@
1 import 'package:cake_wallet/core/execution_state.dart';
2 import 'package:cake_wallet/generated/i18n.dart';
3 +import 'package:cake_wallet/routes.dart';
4 import 'package:cake_wallet/src/screens/base_page.dart';
5 +import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
6 import 'package:cake_wallet/src/screens/integrations/deuro/widgets/info_chip.dart';
7 import 'package:cake_wallet/src/screens/integrations/deuro/widgets/interest_card_widget.dart';
8 import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_card_widget.dart';
@@ -62,7 +64,7 @@ class DEuroSavingsPage extends BasePage {
64 fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat,
65 onAddSavingsPressed: () => _onSavingsAdd(context),
66 onRemoveSavingsPressed: () => _onSavingsRemove(context),
65 - onApproveSavingsPressed: _dEuroViewModel.prepareApproval,
67 + onApproveSavingsPressed: () => _onApproval(context),
68 onTooltipPressed: () => _onSavingsTooltipPressed(context),
69 isEnabled: _dEuroViewModel.isEnabled,
70 isLoading: _dEuroViewModel.isLoading,
@@ -74,8 +76,8 @@ class DEuroSavingsPage extends BasePage {
76 fiatAccruedInterest: _dEuroViewModel.fiatAccruedInterestFormated,
77 fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat,
78 accruedInterest: _dEuroViewModel.accruedInterestFormated,
77 - onCollectInterest: _onCollectInterest,
78 - onReinvestInterest: _onReinvestInterest,
79 + onCollectInterest: () => _onCollectInterest(context),
80 + onReinvestInterest: () => _onReinvestInterest(context),
81 onTooltipPressed: () => _onInterestTooltipPressed(context),
82 isEnabled: _dEuroViewModel.isSavingsActionsEnabled,
83 ),
@@ -117,7 +119,10 @@ class DEuroSavingsPage extends BasePage {
119 if (_editSheetIsOpen) return;
120 _editSheetIsOpen = true;
121 final amount = await _showEditBottomSheet(context, isAdding: true);
120 - if (amount != null) _dEuroViewModel.prepareSavingsEdit(amount, true);
122 + if (amount != null) {
123 + await _requireHardwareWallet(context);
124 + _dEuroViewModel.prepareSavingsEdit(amount, true);
125 + }
126 _editSheetIsOpen = false;
127 }
128
@@ -125,24 +130,52 @@ class DEuroSavingsPage extends BasePage {
130 if (_editSheetIsOpen) return;
131 _editSheetIsOpen = true;
132 final amount = await _showEditBottomSheet(context, isAdding: false);
128 - if (amount != null) _dEuroViewModel.prepareSavingsEdit(amount, false);
129 - _editSheetIsOpen = false;
133 + if (amount != null) {
134 + await _requireHardwareWallet(context);
135 + _dEuroViewModel.prepareSavingsEdit(amount, false);
136 + }
137 + _editSheetIsOpen = false;
138 }
139
132 - Future<void> _onReinvestInterest() async {
140 + Future<void> _onReinvestInterest(BuildContext context) async {
141 if (_editSheetIsOpen) return;
142 _editSheetIsOpen = true;
143 + await _requireHardwareWallet(context);
144 await _dEuroViewModel.prepareReinvestInterest();
145 _editSheetIsOpen = false;
146 }
147
139 - Future<void> _onCollectInterest() async {
148 + Future<void> _onCollectInterest(BuildContext context) async {
149 if (_editSheetIsOpen) return;
150 _editSheetIsOpen = true;
151 + await _requireHardwareWallet(context);
152 await _dEuroViewModel.prepareCollectInterest();
153 _editSheetIsOpen = false;
154 }
155
156 + Future<void> _onApproval(BuildContext context) async {
157 + await _requireHardwareWallet(context);
158 + _dEuroViewModel.prepareApproval();
159 + }
160 +
161 + Future<void> _requireHardwareWallet(BuildContext context) async {
162 + if (_dEuroViewModel.wallet.isHardwareWallet) {
163 + if (!_dEuroViewModel.hardwareWalletViewModel!.isConnected) {
164 + await Navigator.of(context).pushNamed(Routes.connectDevices,
165 + arguments: ConnectDevicePageParams(
166 + walletType: _dEuroViewModel.wallet.type,
167 + hardwareWalletType: _dEuroViewModel.wallet.walletInfo.hardwareWalletType!,
168 + onConnectDevice: (context, _) {
169 + _dEuroViewModel.hardwareWalletViewModel!.initWallet(_dEuroViewModel.wallet);
170 + Navigator.of(context).pop();
171 + },
172 + ));
173 + } else {
174 + _dEuroViewModel.hardwareWalletViewModel!.initWallet(_dEuroViewModel.wallet);
175 + }
176 + }
177 + }
178 +
179 bool _isReactionsSet = false;
180
181 void _setReactions(BuildContext context, DEuroViewModel dEuroViewModel) {
lib/view_model/integrations/deuro_view_model.dart
+8 -6
@@ -7,10 +7,12 @@ import 'package:cake_wallet/store/app_store.dart';
7 import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
8 import 'package:cake_wallet/store/settings_store.dart';
9 import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
10 +import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
11 import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
12 import 'package:cw_core/crypto_currency.dart';
13 import 'package:cw_core/parse_fixed.dart';
14 import 'package:cw_core/pending_transaction.dart';
15 +import 'package:cw_core/wallet_base.dart';
16 import 'package:cw_core/wallet_type.dart';
17 import 'package:mobx/mobx.dart';
18
@@ -23,20 +25,20 @@ abstract class DEuroViewModelBase with Store {
25
26 static BigInt get MIN_ACCRUED_INTEREST => BigInt.parse("1000000000000");
27
26 - DEuroViewModelBase(
27 - this._appStore,
28 - this.balanceViewModel,
29 - this._settingsStore,
30 - this._fiatConversationStore,
31 - ) {
28 + DEuroViewModelBase(this._appStore, this.balanceViewModel, this._settingsStore,
29 + this._fiatConversationStore,
30 + [this.hardwareWalletViewModel]) {
31 reloadInterestRate();
32 reloadSavingsUserData();
33 }
34
35 final BalanceViewModel balanceViewModel;
36 + final HardwareWalletViewModel? hardwareWalletViewModel;
37 final SettingsStore _settingsStore;
38 final FiatConversionStore _fiatConversationStore;
39
40 + WalletBase get wallet => this._appStore.wallet!;
41 +
42 @computed
43 bool get isFiatDisabled => balanceViewModel.isFiatDisabled;
44