Hide broken options in Bitcoin wallets without a private key (#3532)
What changed, and why it matters
This commit hides certain wallet features in the Cake Wallet app when they cannot actually work—specifically for hardware wallets, air-gapped wallets, and wallets that do not contain a private key. The changes prevent users from seeing or tapping options like Payjoin, Lightning, silent payments, message signing/verification, MWEB, and some log exports when those features are unsupported. It is a defensive UI fix rather than a patch for an active exploit.
Treat as a routine hardening/UI consistency fix. Review whether any of the hidden features are still reachable through deep links, shortcuts, or state manipulation, and verify that backend code also rejects unsupported operations rather than relying solely on UI gating.
Security signals we found
UI-level hiding of features that would fail or behave incorrectly on wallets without required keys/capabilities
New capability predicates tied to private-key presence and hardware-wallet type
Reduction of user-facing attack surface where broken options could be invoked
No cryptographic, input-validation, or authorization logic changes observed
Evidence from the diff
The commit adds capability flags to the wallet base class and concrete Bitcoin/Electrum wallet implementations: hasPayjoinSupport, hasLightningSupport, hasSilentPaymentsScanning, canSignMessages, and receiveOptionAvailable(). UI pages and view models now gate related menu items and receive-page options with these flags. For example, Payjoin and Lightning support require a non-empty private key or an initialized Lightning SDK; message signing is hidden for hardware/air-gapped wallets; MWEB is hidden on hardware wallets; and receive-page options are filtered by _wallet.receiveOptionAvailable().
Changed components
cw_bitcoin/lib/bitcoin_wallet.dartcw_bitcoin/lib/electrum_wallet.dartcw_core/lib/wallet_base.dartlib/src/screens/settings/other_settings_page.dartlib/src/screens/settings/privacy_page.dartlib/view_model/dashboard/dashboard_view_model.dartlib/view_model/dashboard/receive_option_view_model.dartlib/view_model/settings/other_settings_view_model.dartlib/view_model/settings/privacy_settings_view_model.dartInspect captured patch +54 / −7
diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart
index 4f791ae..ac40de0 100644
--- a/cw_bitcoin/lib/bitcoin_wallet.dart
+++ b/cw_bitcoin/lib/bitcoin_wallet.dart
@@ -7,6 +7,7 @@ import 'package:cw_bitcoin/.secrets.g.dart' as secrets;
import 'package:cw_bitcoin/address_from_output.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
+import "package:cw_bitcoin/bitcoin_receive_page_option.dart";
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
@@ -33,6 +34,7 @@ import 'package:cw_core/output_info.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
+import "package:cw_core/receive_page_option.dart";
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
@@ -417,6 +419,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
late final PayjoinManager payjoinManager;
+ @override
+ bool get hasPayjoinSupport => keys.privateKey.isNotEmpty;
+
+ @override
+ bool get hasLightningSupport => lightningWallet?.sdk != null;
+
bool get isPayjoinAvailable => unspentCoinsInfo.values
.where((element) => element.walletId == id && element.isSending && !element.isFrozen)
.isNotEmpty;
@@ -673,4 +681,17 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
return super.signMessage(message, address: address);
}
+
+ @override
+ bool receiveOptionAvailable(ReceivePageOption option) {
+ if(option == BitcoinReceivePageOption.lightning) {
+ return hasLightningSupport;
+ }
+
+ if(option == BitcoinReceivePageOption.silent_payments) {
+ return hasSilentPaymentsScanning;
+ }
+
+ return true;
+ }
}
diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart
index c82ac8e..1da0169 100644
--- a/cw_bitcoin/lib/electrum_wallet.dart
+++ b/cw_bitcoin/lib/electrum_wallet.dart
@@ -407,6 +407,7 @@ abstract class ElectrumWalletBase
@override
bool isTestnet;
+ @override
bool get hasSilentPaymentsScanning => type == WalletType.bitcoin && keys.privateKey.isNotEmpty;
@observable
diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart
index 89d6356..024095b 100644
--- a/cw_core/lib/wallet_base.dart
+++ b/cw_core/lib/wallet_base.dart
@@ -1,3 +1,4 @@
+import "package:cw_core/receive_page_option.dart";
import 'package:mobx/mobx.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/transaction_info.dart';
@@ -136,4 +137,14 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
/// Each wallet implementation should override this to make a single, efficient call
/// Returns true if the node is healthy, false otherwise
Future<bool> checkNodeHealth();
+
+ bool get hasPayjoinSupport => false;
+ bool get hasLightningSupport => false;
+ bool get hasSilentPaymentsScanning => false;
+
+ // hardware wallet - bitbox, ledger, trezor.
+ // we also have airgap wallets but those can't sign messages
+ bool get canSignMessages => walletInfo.hardwareWalletType == null || walletInfo.isHardwareWallet;
+
+ bool receiveOptionAvailable(ReceivePageOption option) => true;
}
diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart
index 3f52919..512f719 100644
--- a/lib/src/screens/settings/other_settings_page.dart
+++ b/lib/src/screens/settings/other_settings_page.dart
@@ -143,6 +143,7 @@ class OtherSettingsPage extends BasePage {
},
}),
),
+ if(_otherSettingsViewModel.hasSignVerify)
ListItemRegularRow(
keyValue: "security_backup_page_sign_and_verify",
label: S.of(context).sign_verify_title,
@@ -152,10 +153,12 @@ class OtherSettingsPage extends BasePage {
],
if (_otherSettingsViewModel.walletType == WalletType.bitcoin)
"btc_logging": [
+ if(_otherSettingsViewModel.hasLightning)
ListItemRegularRow(
keyValue: "export_lightning_logs",
label: S.of(context).export_lightning_logs,
onTap: () => onExportLNLog(context)),
+ if(_otherSettingsViewModel.hasPayjoin)
ListItemRegularRow(
keyValue: "export_payjoin_logs",
label: S.of(context).export_payjoin_logs,
diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart
index 3e35f08..241202f 100644
--- a/lib/src/screens/settings/privacy_page.dart
+++ b/lib/src/screens/settings/privacy_page.dart
@@ -76,7 +76,7 @@ class PrivacyPage extends BasePage {
}),
],
"": [
- if (_privacySettingsViewModel.isBitcoin)
+ if (_privacySettingsViewModel.hasSilentPaymentsScanning)
ListItemRegularRow(
iconPath: "assets/new-ui/settings_row_icons/silent-payments.svg",
keyValue: "silent_payments",
diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart
index fb2d53b..e74771b 100644
--- a/lib/view_model/dashboard/dashboard_view_model.dart
+++ b/lib/view_model/dashboard/dashboard_view_model.dart
@@ -635,7 +635,7 @@ abstract class DashboardViewModelBase with Store {
if (settingsStore.mwebAdDismissed) return false;
- return Platform.isAndroid || Platform.isIOS;
+ return (Platform.isAndroid || Platform.isIOS) && !wallet.isHardwareWallet;
}
@action
diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart
index 0f3afef..f82fd9e 100644
--- a/lib/view_model/dashboard/receive_option_view_model.dart
+++ b/lib/view_model/dashboard/receive_option_view_model.dart
@@ -35,7 +35,8 @@ abstract class ReceiveOptionViewModelBase with Store {
ReceivePageOption selectedReceiveOption;
@computed
- List<ReceivePageOption> get options => _wallet.walletAddresses.receivePageOptions;
+ List<ReceivePageOption> get options =>
+ _wallet.walletAddresses.receivePageOptions.where(_wallet.receiveOptionAvailable).toList();
String get walletTypeString => walletTypeToString(_wallet.type);
diff --git a/lib/view_model/settings/other_settings_view_model.dart b/lib/view_model/settings/other_settings_view_model.dart
index 36ede08..fa3a274 100644
--- a/lib/view_model/settings/other_settings_view_model.dart
+++ b/lib/view_model/settings/other_settings_view_model.dart
@@ -47,6 +47,8 @@ abstract class OtherSettingsViewModelBase with Store {
final SettingsStore _settingsStore;
final SendViewModel sendViewModel;
+ bool get hasSignVerify => _wallet.canSignMessages;
+
@computed
TransactionPriority get transactionPriority {
final priority = _settingsStore.getPriority(walletType, chainId: chainId);
@@ -173,6 +175,10 @@ abstract class OtherSettingsViewModelBase with Store {
return null;
}
+ bool get hasPayjoin => _wallet.hasPayjoinSupport;
+
+ bool get hasLightning => _wallet.hasLightningSupport;
+
Future<File?> getLightningLog() async {
final path = await pathForWalletDir(name: _wallet.name, type: walletType);
final logFile = File("$path/lightning.log");
diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart
index 5c1a412..6eeabc0 100644
--- a/lib/view_model/settings/privacy_settings_view_model.dart
+++ b/lib/view_model/settings/privacy_settings_view_model.dart
@@ -25,7 +25,12 @@ abstract class PrivacySettingsViewModelBase with Store {
bool get isBitcoin => _wallet.type == WalletType.bitcoin;
@computed
- bool get hasMWEB => _wallet.type == WalletType.litecoin && (Platform.isIOS || Platform.isAndroid);
+ bool get hasSilentPaymentsScanning => _wallet.hasSilentPaymentsScanning;
+
+ @computed
+ bool get hasMWEB =>
+ _wallet.type == WalletType.litecoin && (Platform.isIOS || Platform.isAndroid) &&
+ !_wallet.isHardwareWallet;
@computed
bool get isAutoGenerateSubaddressesEnabled =>
@@ -108,11 +113,10 @@ abstract class PrivacySettingsViewModelBase with Store {
bool get usePayjoin => _settingsStore.usePayjoin;
@computed
- bool get canUsePayjoin => _wallet.type == WalletType.bitcoin && DeviceInfo.instance.isMobile;
+ bool get canUsePayjoin => _wallet.hasPayjoinSupport && DeviceInfo.instance.isMobile;
@computed
- bool get canUseLightning =>
- _wallet.type == WalletType.bitcoin && !Platform.isWindows && !Platform.isLinux;
+ bool get canUseLightning => _wallet.hasLightningSupport;
@computed
bool get useLightning => _wallet.type == WalletType.bitcoin && bitcoin!.useLightning(_wallet);
Why this scored 25/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.