Mweb fixes (#1972)

* quick fix [still investigating] * cleaner quick fix * cleaner quick fix * log fix * save to downloads would require an additional permission * change address handling updates [skip ci] * should change address not updating * change address handling updates * remove unnecessary code + don't reset change address index to 0

Matthew Fosse committed Jan 24, 2025 at 10:09 UTC 199ada3fa9dbc6c5e6da4f7c7f2c8cfef9692674
7 files changed +55 -60
cw_bitcoin/lib/bitcoin_wallet_addresses.dart
+5 -1
@@ -2,6 +2,7 @@ import 'package:bitcoin_base/bitcoin_base.dart';
2 import 'package:blockchain_utils/bip/bip/bip32/bip32.dart';
3 import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
4 import 'package:cw_bitcoin/utils.dart';
5 +import 'package:cw_core/unspent_coin_type.dart';
6 import 'package:cw_core/wallet_info.dart';
7 import 'package:mobx/mobx.dart';
8
@@ -26,7 +27,10 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
27
28 @override
29 String getAddress(
29 - {required int index, required Bip32Slip10Secp256k1 hd, BitcoinAddressType? addressType}) {
30 + {required int index,
31 + required Bip32Slip10Secp256k1 hd,
32 + BitcoinAddressType? addressType,
33 + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) {
34 if (addressType == P2pkhAddressType.p2pkh)
35 return generateP2PKHAddress(hd: hd, index: index, network: network);
36
cw_bitcoin/lib/electrum_wallet.dart
+1
@@ -842,6 +842,7 @@ abstract class ElectrumWalletBase
842 final changeAddress = await walletAddresses.getChangeAddress(
843 inputs: utxoDetails.availableInputs,
844 outputs: updatedOutputs,
845 + coinTypeToSpendFrom: coinTypeToSpendFrom,
846 );
847 final address = RegexUtils.addressTypeFromStr(changeAddress.address, network);
848 updatedOutputs.add(BitcoinOutput(
cw_bitcoin/lib/electrum_wallet_addresses.dart
+11 -8
@@ -4,6 +4,7 @@ import 'package:bitcoin_base/bitcoin_base.dart';
4 import 'package:blockchain_utils/blockchain_utils.dart';
5 import 'package:cw_bitcoin/bitcoin_address_record.dart';
6 import 'package:cw_bitcoin/electrum_wallet.dart';
7 +import 'package:cw_core/unspent_coin_type.dart';
8 import 'package:cw_core/utils/print_verbose.dart';
9 import 'package:cw_bitcoin/bitcoin_unspent.dart';
10 import 'package:cw_core/wallet_addresses.dart';
@@ -47,7 +48,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
48 List<BitcoinAddressRecord>? initialMwebAddresses,
49 Bip32Slip10Secp256k1? masterHd,
50 BitcoinAddressType? initialAddressPageType,
50 -
51 }) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
52 addressesByReceiveType =
53 ObservableList<BaseBitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
@@ -187,13 +187,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
187 return;
188 }
189 try {
190 - final addressRecord = _addresses.firstWhere(
191 - (addressRecord) => addressRecord.address == addr,
192 - );
190 + final addressRecord = _addresses.firstWhere(
191 + (addressRecord) => addressRecord.address == addr,
192 + );
193
194 - previousAddressRecord = addressRecord;
195 - receiveAddresses.remove(addressRecord);
196 - receiveAddresses.insert(0, addressRecord);
194 + previousAddressRecord = addressRecord;
195 + receiveAddresses.remove(addressRecord);
196 + receiveAddresses.insert(0, addressRecord);
197 } catch (e) {
198 printV("ElectrumWalletAddressBase: set address ($addr): $e");
199 }
@@ -274,7 +274,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
274 }
275
276 @action
277 - Future<BitcoinAddressRecord> getChangeAddress({List<BitcoinUnspent>? inputs, List<BitcoinOutput>? outputs, bool isPegIn = false}) async {
277 + Future<BitcoinAddressRecord> getChangeAddress(
278 + {List<BitcoinUnspent>? inputs,
279 + List<BitcoinOutput>? outputs,
280 + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) async {
281 updateChangeAddresses();
282
283 if (changeAddresses.isEmpty) {
cw_bitcoin/lib/litecoin_wallet.dart
+21 -12
@@ -9,6 +9,7 @@ import 'package:crypto/crypto.dart';
9 import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
10 import 'package:cw_core/cake_hive.dart';
11 import 'package:cw_core/mweb_utxo.dart';
12 +import 'package:cw_core/unspent_coin_type.dart';
13 import 'package:cw_core/utils/print_verbose.dart';
14 import 'package:cw_core/node.dart';
15 import 'package:cw_mweb/mwebd.pbgrpc.dart';
@@ -394,7 +395,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
395 // if the confirmations haven't changed, skip updating:
396 if (tx.confirmations == confirmations) continue;
397
397 -
398 // if an outgoing tx is now confirmed, delete the utxo from the box (delete the unspent coin):
399 if (confirmations >= 2 &&
400 tx.direction == TransactionDirection.outgoing &&
@@ -966,10 +966,19 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
966 List<ECPrivateInfo>? inputPrivKeyInfos,
967 List<Outpoint>? vinOutpoints,
968 }) async {
969 - final spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb);
970 - final paysToMweb = outputs
969 + bool spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb);
970 + bool paysToMweb = outputs
971 .any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb);
972 - if (!spendsMweb && !paysToMweb) {
972 +
973 + bool isRegular = !spendsMweb && !paysToMweb;
974 + bool isMweb = spendsMweb || paysToMweb;
975 +
976 + if (isMweb && !mwebEnabled) {
977 + throw Exception("MWEB is not enabled! can't calculate fee without starting the mweb server!");
978 + // TODO: likely the change address is mweb and just not updated
979 + }
980 +
981 + if (isRegular) {
982 return await super.calcFee(
983 utxos: utxos,
984 outputs: outputs,
@@ -981,10 +990,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
990 );
991 }
992
984 - if (!mwebEnabled) {
985 - throw Exception("MWEB is not enabled! can't calculate fee without starting the mweb server!");
986 - }
987 -
993 if (outputs.length == 1 && outputs[0].toOutput.amount == BigInt.zero) {
994 outputs = [
995 BitcoinScriptOutput(
@@ -1055,7 +1060,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
1060
1061 if (!mwebEnabled) {
1062 tx.changeAddressOverride =
1058 - (await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(isPegIn: false))
1063 + (await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(coinTypeToSpendFrom: UnspentCoinType.nonMweb))
1064 .address;
1065 return tx;
1066 }
@@ -1103,13 +1108,15 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
1108 }
1109 }
1110
1111 + // could probably be simplified but left for clarity:
1112 bool isPegIn = !hasMwebInput && hasMwebOutput;
1113 bool isPegOut = hasMwebInput && hasRegularOutput;
1114 bool isRegular = !hasMwebInput && !hasMwebOutput;
1115 + bool shouldNotUseMwebChange = isPegIn || isRegular || !hasMwebInput;
1116 tx.changeAddressOverride = (await (walletAddresses as LitecoinWalletAddresses)
1110 - .getChangeAddress(isPegIn: isPegIn || isRegular))
1117 + .getChangeAddress(coinTypeToSpendFrom: shouldNotUseMwebChange ? UnspentCoinType.nonMweb : UnspentCoinType.any))
1118 .address;
1112 - if (!hasMwebInput && !hasMwebOutput) {
1119 + if (isRegular) {
1120 tx.isMweb = false;
1121 return tx;
1122 }
@@ -1212,7 +1219,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
1219 }
1220
1221 Future<void> setMwebEnabled(bool enabled) async {
1215 - if (mwebEnabled == enabled) {
1222 + if (mwebEnabled == enabled &&
1223 + alwaysScan == enabled &&
1224 + (walletAddresses as LitecoinWalletAddresses).mwebEnabled == enabled) {
1225 return;
1226 }
1227
cw_bitcoin/lib/litecoin_wallet_addresses.dart
+9 -8
@@ -9,6 +9,7 @@ import 'package:cw_bitcoin/bitcoin_unspent.dart';
9 import 'package:cw_bitcoin/electrum_wallet.dart';
10 import 'package:cw_bitcoin/utils.dart';
11 import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
12 +import 'package:cw_core/unspent_coin_type.dart';
13 import 'package:cw_core/utils/print_verbose.dart';
14 import 'package:cw_core/wallet_info.dart';
15 import 'package:cw_mweb/cw_mweb.dart';
@@ -148,10 +149,12 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
149 @action
150 @override
151 Future<BitcoinAddressRecord> getChangeAddress(
151 - {List<BitcoinUnspent>? inputs, List<BitcoinOutput>? outputs, bool isPegIn = false}) async {
152 + {List<BitcoinUnspent>? inputs,
153 + List<BitcoinOutput>? outputs,
154 + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) async {
155 // use regular change address on peg in, otherwise use mweb for change address:
156
154 - if (!mwebEnabled || isPegIn) {
157 + if (!mwebEnabled || coinTypeToSpendFrom == UnspentCoinType.nonMweb) {
158 return super.getChangeAddress();
159 }
160
@@ -178,19 +181,17 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
181 });
182
183 bool isPegIn = !comesFromMweb && outputsToMweb;
184 + bool isNonMweb = !comesFromMweb && !outputsToMweb;
185
182 - if (isPegIn && mwebEnabled) {
183 - return super.getChangeAddress();
184 - }
185 -
186 - // use regular change address if it's not an mweb tx:
187 - if (!comesFromMweb && !outputsToMweb) {
186 + // use regular change address if it's not an mweb tx or if it's a peg in:
187 + if (isPegIn || isNonMweb) {
188 return super.getChangeAddress();
189 }
190 }
191
192 if (mwebEnabled) {
193 await ensureMwebAddressUpToIndexExists(1);
194 + updateChangeAddresses();
195 return BitcoinAddressRecord(
196 mwebAddrs[0],
197 index: 0,
lib/src/screens/settings/mweb_logs_page.dart
+6 -12
@@ -77,15 +77,12 @@ class MwebLogsPage extends BasePage {
77 return AlertWithTwoActions(
78 alertTitle: S.of(context).export_backup,
79 alertContent: S.of(context).select_destination,
80 - rightButtonText: S.of(context).save_to_downloads,
81 - leftButtonText: S.of(context).share,
82 - actionRightButton: () async {
83 - const downloadDirPath = "/storage/emulated/0/Download";
84 - final filePath = downloadDirPath + "/debug.log";
85 - await mwebSettingsViewModelBase.saveLogsLocally(filePath);
80 + rightButtonText: S.of(context).save,
81 + leftButtonText: S.of(context).cancel,
82 + actionLeftButton: () async {
83 Navigator.of(dialogContext).pop();
84 },
88 - actionLeftButton: () async {
85 + actionRightButton: () async {
86 Navigator.of(dialogContext).pop();
87 try {
88 await share(context);
@@ -101,11 +98,8 @@ class MwebLogsPage extends BasePage {
98 }
99
100 Future<void> share(BuildContext context) async {
104 - final filePath = (await getAppDir()).path + "/debug.log";
105 - bool success = await mwebSettingsViewModelBase.saveLogsLocally(filePath);
106 - if (!success) return;
107 - await ShareUtil.shareFile(filePath: filePath, fileName: "debug.log", context: context);
108 - await mwebSettingsViewModelBase.removeLogsLocally(filePath);
101 + final inAppPath = "${(await getApplicationSupportDirectory()).path}/logs/debug.log";
102 + await ShareUtil.shareFile(filePath: inAppPath, fileName: "debug.log", context: context);
103 }
104
105 Future<void> _saveFile() async {
lib/view_model/settings/mweb_settings_view_model.dart
+2 -19
@@ -3,6 +3,8 @@ import 'dart:io';
3 import 'package:cake_wallet/bitcoin/bitcoin.dart';
4 import 'package:cake_wallet/store/settings_store.dart';
5 import 'package:cake_wallet/utils/exception_handler.dart';
6 +import 'package:cw_core/root_dir.dart';
7 +import 'package:cw_core/utils/print_verbose.dart';
8 import 'package:cw_core/wallet_base.dart';
9 import 'package:flutter/widgets.dart';
10 import 'package:mobx/mobx.dart';
@@ -47,25 +49,6 @@ abstract class MwebSettingsViewModelBase with Store {
49 _settingsStore.mwebAlwaysScan = value;
50 }
51
50 - Future<bool> saveLogsLocally(String filePath) async {
51 - try {
52 - final appSupportPath = (await getApplicationSupportDirectory()).path;
53 - final logsFile = File("$appSupportPath/logs/debug.log");
54 - if (!logsFile.existsSync()) {
55 - throw Exception('Logs file does not exist');
56 - }
57 - await logsFile.copy(filePath);
58 - return true;
59 - } catch (e, s) {
60 - ExceptionHandler.onError(FlutterErrorDetails(
61 - exception: e,
62 - stack: s,
63 - library: "Export Logs",
64 - ));
65 - return false;
66 - }
67 - }
68 -
52 Future<String> getAbbreviatedLogs() async {
53 final appSupportPath = (await getApplicationSupportDirectory()).path;
54 final logsFile = File("$appSupportPath/logs/debug.log");