CW-994 mweb enhancements (#2204)
What changed, and why it matters
This update improves how Cake Wallet handles Litecoin's optional privacy feature (MWEB). It fixes a bug where the wallet might accidentally try to spend MWEB coins even when MWEB was turned off, which could cause failed or confusing transactions. It also makes the wallet more precise about detecting MWEB addresses and adds a link to a MWEB block explorer in transaction details. There is no clear evidence this is a security emergency, but it does fix a functional bug that could affect user funds or transaction reliability.
Treat as a routine bug-fix update. Users relying on Litecoin MWEB should update to avoid transaction failures when MWEB is disabled. No urgent security response is indicated by the commit alone.
Security signals we found
Functional bug fix that could prevent unintended coin selection behavior
Address parsing tightened from substring to prefix match
UI addition linking to external MWEB block explorer
No explicit security disclosure or CVE referenced
Evidence from the diff
The commit modifies Litecoin wallet logic to prevent spending from MWEB UTXOs when MWEB is disabled. It does this by overriding coinTypeToSpendFrom to UnspentCoinType.nonMweb both in ElectrumWalletBase transaction building and in LitecoinWalletBase.createTransaction. It also tightens MWEB address detection from substring contains(‘mweb’) to prefix startsWith(‘ltcmweb’), and adds a UI block explorer link for MWEB transactions. A path_provider import is removed. The changes are defensive bug fixes rather than a full security patch.
Changed components
cw_bitcoin/lib/electrum_wallet.dartcw_bitcoin/lib/litecoin_wallet.dartcw_bitcoin/lib/litecoin_wallet_addresses.dartlib/view_model/transaction_details_view_model.dartInspect captured patch +54 / −7
diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart
index dbd3fe8a..477544ee 100644
--- a/cw_bitcoin/lib/electrum_wallet.dart
+++ b/cw_bitcoin/lib/electrum_wallet.dart
@@ -11,7 +11,6 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_bitcoin/bitcoin_wallet.dart';
import 'package:cw_bitcoin/litecoin_wallet.dart';
-import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:collection/collection.dart';
@@ -899,6 +898,13 @@ abstract class ElectrumWalletBase
throw BitcoinTransactionNoDustException();
}
+ // if mweb isn't enabled, don't consider spending mweb coins:
+ if (this is LitecoinWallet) {
+ var mwebEnabled = (this as LitecoinWallet).mwebEnabled;
+ if (!mwebEnabled) {
+ coinTypeToSpendFrom = UnspentCoinType.nonMweb;
+ }
+ }
// If there is only one output, and the amount to send is more than the max spendable amount
// then it is actually a send all transaction
diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart
index 91d927da..bdbbe3f6 100644
--- a/cw_bitcoin/lib/litecoin_wallet.dart
+++ b/cw_bitcoin/lib/litecoin_wallet.dart
@@ -886,6 +886,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
if (unspent.vout == 0) {
unspent.isChange = true;
}
+
+ // printV("unspent: $unspent ${unspent.vout} ${utxo.value}");
mwebUnspentCoins.add(unspent);
});
@@ -1140,13 +1142,27 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
try {
- var tx = await super.createTransaction(credentials) as PendingBitcoinTransaction;
+ var creds;
+ if (!mwebEnabled) {
+ BitcoinTransactionCredentials btcCreds = (credentials as BitcoinTransactionCredentials);
+ // sets unspent coin type to nonMweb:
+ creds = BitcoinTransactionCredentials(
+ btcCreds.outputs,
+ priority: btcCreds.priority,
+ feeRate: btcCreds.feeRate,
+ coinTypeToSpendFrom: UnspentCoinType.nonMweb,
+ );
+ } else {
+ creds = credentials;
+ }
+ var tx = await super.createTransaction(creds as Object) as PendingBitcoinTransaction;
tx.isMweb = mwebEnabled;
if (!mwebEnabled) {
tx.changeAddressOverride = (await (walletAddresses as LitecoinWalletAddresses)
.getChangeAddress(coinTypeToSpendFrom: UnspentCoinType.nonMweb))
.address;
+
if (tx.shouldCommitUR()) {
tx.unsignedPsbt = await buildPsbt(tx, false);
}
@@ -1165,17 +1181,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
final address = output.address.toLowerCase();
final extractedAddress = output.extractedAddress?.toLowerCase();
- if (address.contains("mweb")) {
+ if (address.startsWith("ltcmweb")) {
hasMwebOutput = true;
}
- if (!address.contains("mweb")) {
+ if (!address.startsWith("ltcmweb")) {
hasRegularOutput = true;
}
if (extractedAddress != null && extractedAddress.isNotEmpty) {
- if (extractedAddress.contains("mweb")) {
+ if (extractedAddress.startsWith("ltcmweb")) {
hasMwebOutput = true;
}
- if (!extractedAddress.contains("mweb")) {
+ if (!extractedAddress.startsWith("ltcmweb")) {
hasRegularOutput = true;
}
}
diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart
index 9b321186..1493fbfb 100644
--- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart
+++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart
@@ -188,7 +188,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
if (!element.isSending || element.isFrozen) {
return;
}
- if (element.address.contains("mweb")) {
+ if (element.address.startsWith("ltcmweb")) {
comesFromMweb = true;
}
});
diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart
index ffe22fac..810d08c1 100644
--- a/lib/view_model/transaction_details_view_model.dart
+++ b/lib/view_model/transaction_details_view_model.dart
@@ -59,6 +59,7 @@ abstract class TransactionDetailsViewModelBase with Store {
if (!canReplaceByFee) _checkForRBF(tx);
break;
case WalletType.litecoin:
+ _addLitecoinListItems(tx, dateFormat);
case WalletType.bitcoinCash:
_addElectrumListItems(tx, dateFormat);
break;
@@ -388,6 +389,30 @@ abstract class TransactionDetailsViewModelBase with Store {
items.addAll(_items);
}
+ void _addLitecoinListItems(TransactionInfo tx, DateFormat dateFormat) {
+ _addElectrumListItems(tx, dateFormat);
+
+ bool isMweb = bitcoin!.txIsMweb(tx);
+
+ final _items = [
+ if (isMweb)
+ BlockExplorerListItem(
+ title: S.current.view_in_block_explorer,
+ value: S.current.view_transaction_on + 'mwebexplorer.com',
+ onTap: () async {
+ try {
+ final uri = Uri.parse('https://www.mwebexplorer.com/blocks/block/${tx.height}');
+ if (await canLaunchUrl(uri))
+ await launchUrl(uri, mode: LaunchMode.externalApplication);
+ } catch (e) {}
+ },
+ key: ValueKey('block_explorer_list_item_mweb_wallet_type_key'),
+ ),
+ ];
+
+ items.addAll(_items);
+ }
+
void _addHavenListItems(TransactionInfo tx, DateFormat dateFormat) {
items.addAll([
StandartListItem(
Why this scored 32/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.