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